devin-cli 1.3.4__tar.gz → 1.3.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. {devin_cli-1.3.4 → devin_cli-1.3.5}/PKG-INFO +1 -1
  2. {devin_cli-1.3.4 → devin_cli-1.3.5}/pyproject.toml +1 -1
  3. devin_cli-1.3.5/src/devin_cli/__init__.py +1 -0
  4. {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/cli.py +316 -12
  5. devin_cli-1.3.5/tests/test_cli_extended.py +121 -0
  6. devin_cli-1.3.4/src/devin_cli/__init__.py +0 -1
  7. {devin_cli-1.3.4 → devin_cli-1.3.5}/.github/workflows/pypi-publish.yml +0 -0
  8. {devin_cli-1.3.4 → devin_cli-1.3.5}/.gitignore +0 -0
  9. {devin_cli-1.3.4 → devin_cli-1.3.5}/CHANGELOG.md +0 -0
  10. {devin_cli-1.3.4 → devin_cli-1.3.5}/LICENSE +0 -0
  11. {devin_cli-1.3.4 → devin_cli-1.3.5}/README.md +0 -0
  12. {devin_cli-1.3.4 → devin_cli-1.3.5}/assets/logo.png +0 -0
  13. {devin_cli-1.3.4 → devin_cli-1.3.5}/generate_facades.py +0 -0
  14. {devin_cli-1.3.4 → devin_cli-1.3.5}/setup.py +0 -0
  15. {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/__init__.py +0 -0
  16. {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/attachments.py +0 -0
  17. {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/client.py +0 -0
  18. {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/consumption.py +0 -0
  19. {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/knowledge.py +0 -0
  20. {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/members.py +0 -0
  21. {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/organizations.py +0 -0
  22. {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/playbooks.py +0 -0
  23. {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/repositories.py +0 -0
  24. {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/schedules.py +0 -0
  25. {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/secrets.py +0 -0
  26. {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/sessions.py +0 -0
  27. {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/v1/__init__.py +0 -0
  28. {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/v1/attachments.py +0 -0
  29. {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/v1/knowledge.py +0 -0
  30. {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/v1/playbooks.py +0 -0
  31. {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/v1/secrets.py +0 -0
  32. {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/v1/sessions.py +0 -0
  33. {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/v3/__init__.py +0 -0
  34. {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/v3/attachments.py +0 -0
  35. {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/v3/consumption.py +0 -0
  36. {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/v3/knowledge.py +0 -0
  37. {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/v3/members.py +0 -0
  38. {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/v3/organizations.py +0 -0
  39. {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/v3/playbooks.py +0 -0
  40. {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/v3/repositories.py +0 -0
  41. {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/v3/schedules.py +0 -0
  42. {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/v3/secrets.py +0 -0
  43. {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/v3/sessions.py +0 -0
  44. {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/config.py +0 -0
  45. {devin_cli-1.3.4 → devin_cli-1.3.5}/tests/__init__.py +0 -0
  46. {devin_cli-1.3.4 → devin_cli-1.3.5}/tests/conftest.py +0 -0
  47. {devin_cli-1.3.4 → devin_cli-1.3.5}/tests/test_api/test_attachments.py +0 -0
  48. {devin_cli-1.3.4 → devin_cli-1.3.5}/tests/test_api/test_knowledge.py +0 -0
  49. {devin_cli-1.3.4 → devin_cli-1.3.5}/tests/test_api/test_playbooks.py +0 -0
  50. {devin_cli-1.3.4 → devin_cli-1.3.5}/tests/test_api/test_repositories.py +0 -0
  51. {devin_cli-1.3.4 → devin_cli-1.3.5}/tests/test_api/test_schedules.py +0 -0
  52. {devin_cli-1.3.4 → devin_cli-1.3.5}/tests/test_api/test_secrets.py +0 -0
  53. {devin_cli-1.3.4 → devin_cli-1.3.5}/tests/test_api/test_sessions.py +0 -0
  54. {devin_cli-1.3.4 → devin_cli-1.3.5}/tests/test_cli.py +0 -0
  55. {devin_cli-1.3.4 → devin_cli-1.3.5}/tests/test_config.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devin-cli
3
- Version: 1.3.4
3
+ Version: 1.3.5
4
4
  Summary: Unofficial CLI for Devin AI - The first AI Software Engineer
5
5
  Project-URL: Homepage, https://github.com/revanthpobala/devin-cli
6
6
  Project-URL: Repository, https://github.com/revanthpobala/devin-cli.git
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "devin-cli"
3
- version = "1.3.4"
3
+ version = "1.3.5"
4
4
  description = "Unofficial CLI for Devin AI - The first AI Software Engineer"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -0,0 +1 @@
1
+ __version__ = "1.3.5"
@@ -311,6 +311,20 @@ def create_session_cmd(
311
311
  console.print("[yellow]Session created, but no ID returned immediately (awaiting advanced mode setup).[/yellow]")
312
312
 
313
313
  return resp
314
+ def get_stable_terminal_state(status_enum: Optional[str]) -> str:
315
+ if not status_enum:
316
+ return "running"
317
+ status = status_enum.lower()
318
+ if status == "finished":
319
+ return "completed"
320
+ elif status in ("failed", "error"):
321
+ return "failed"
322
+ elif status in ("stopped", "cancelled", "terminated"):
323
+ return "terminated"
324
+ elif status == "blocked":
325
+ return "blocked"
326
+ else:
327
+ return "running"
314
328
 
315
329
 
316
330
  @session_app.command("list")
@@ -325,6 +339,10 @@ def list_sessions_cmd(
325
339
  resp = sessions.list_sessions(limit=limit)
326
340
  sess_list = resp.get("items", resp.get("sessions", []))
327
341
 
342
+ for s in sess_list:
343
+ if isinstance(s, dict):
344
+ s["terminal_state"] = get_stable_terminal_state(s.get("status_enum"))
345
+
328
346
  if not sess_list and config.api_token and config.api_token.startswith("cog_"):
329
347
  console.print("[yellow]Warning: Service tokens (cog_) may only have visibility into sessions they explicitly created, not all organization sessions.[/yellow]")
330
348
 
@@ -334,13 +352,20 @@ def list_sessions_cmd(
334
352
  table = Table(title="Devin Sessions")
335
353
  table.add_column("ID", style="cyan")
336
354
  table.add_column("Status", style="magenta")
355
+ table.add_column("Terminal State", style="blue")
337
356
  table.add_column("Title")
338
357
  for s in sess_list:
339
- table.add_row(s.get("session_id"), s.get("status_enum"), s.get("title") or s.get("prompt", "")[:50])
358
+ table.add_row(
359
+ s.get("session_id"),
360
+ s.get("status_enum"),
361
+ s.get("terminal_state"),
362
+ s.get("title") or s.get("prompt", "")[:50]
363
+ )
340
364
  console.print(table)
341
365
 
342
366
  return resp
343
367
 
368
+
344
369
  @session_app.command("get")
345
370
  @handle_api_error
346
371
  def get_session_cmd(
@@ -351,6 +376,8 @@ def get_session_cmd(
351
376
  if org: config.temporary_org_id = org
352
377
  sid = session_id or get_current_session_id()
353
378
  resp = sessions.get_session(sid)
379
+ if isinstance(resp, dict):
380
+ resp["terminal_state"] = get_stable_terminal_state(resp.get("status_enum"))
354
381
  console.print(Panel(json.dumps(resp, indent=2), title=f"Session {sid}"))
355
382
 
356
383
  return resp
@@ -456,6 +483,98 @@ def terminate_session_cmd(
456
483
  console.print(f"[green]Session {sid} terminated.[/green]")
457
484
  return None
458
485
 
486
+
487
+ def parse_duration(duration_str: str) -> float:
488
+ if not duration_str:
489
+ return 0.0
490
+ duration_str = duration_str.strip().lower()
491
+ val = 0.0
492
+ if duration_str.endswith("s"):
493
+ val = float(duration_str[:-1])
494
+ elif duration_str.endswith("m"):
495
+ val = float(duration_str[:-1]) * 60
496
+ elif duration_str.endswith("h"):
497
+ val = float(duration_str[:-1]) * 3600
498
+ else:
499
+ try:
500
+ val = float(duration_str)
501
+ except ValueError:
502
+ raise ValueError(f"Invalid duration format: '{duration_str}'. Use format like 10s, 5m, 1h.")
503
+ if val < 0:
504
+ raise ValueError("Timeout duration cannot be negative.")
505
+ return val
506
+
507
+
508
+ @session_app.command("wait")
509
+ @handle_api_error
510
+ def wait_session_cmd(
511
+ session_id: Optional[str] = typer.Argument(None, help="Session ID to wait for (defaults to active session)"),
512
+ until: str = typer.Option("completed", "--until", help="Terminal state to wait for (completed | terminal)"),
513
+ timeout: str = typer.Option("1h", "--timeout", help="Timeout duration (e.g. 10s, 5m, 1h)"),
514
+ interval: int = typer.Option(5, "--interval", help="Polling interval in seconds"),
515
+ org: Optional[str] = typer.Option(None, "--org"),
516
+ ):
517
+ """Block until the session reaches a terminal state or the desired state."""
518
+ if org: config.temporary_org_id = org
519
+ sid = session_id or get_current_session_id()
520
+
521
+ until = until.lower()
522
+ if until not in ("completed", "terminal"):
523
+ console.print(f"[bold red]Error:[/bold red] --until must be 'completed' or 'terminal'.")
524
+ raise typer.Exit(1)
525
+
526
+ try:
527
+ timeout_seconds = parse_duration(timeout)
528
+ except ValueError as e:
529
+ console.print(f"[bold red]Error:[/bold red] {e}")
530
+ raise typer.Exit(1)
531
+
532
+ start_time = time.time()
533
+
534
+ console.print(f"[dim]Waiting for session {sid} to reach '{until}' (timeout {timeout}, polling every {interval}s)...[/dim]")
535
+
536
+ try:
537
+ with console.status(f"[bold green]Waiting for session {sid}...[/bold green]") as status:
538
+ while True:
539
+ elapsed = time.time() - start_time
540
+ if timeout_seconds > 0 and elapsed >= timeout_seconds:
541
+ console.print(f"\n[bold red]Timeout:[/bold red] Timed out waiting for session {sid} after {timeout}")
542
+ raise typer.Exit(1)
543
+
544
+ try:
545
+ resp = sessions.get_session(sid)
546
+ except Exception as e:
547
+ console.print(f"\n[bold red]Error fetching session:[/bold red] {e}")
548
+ raise typer.Exit(1)
549
+
550
+ status_enum = resp.get("status_enum")
551
+ term_state = get_stable_terminal_state(status_enum)
552
+
553
+ status.update(f"[bold green]Current Status: {status_enum} (Terminal State: {term_state})[/bold green]")
554
+
555
+ if term_state != "running" and term_state != "blocked":
556
+ if until == "completed" and term_state == "completed":
557
+ console.print(f"\n[bold green]Success:[/bold green] Session {sid} completed successfully.")
558
+ raise typer.Exit(0)
559
+ elif until == "completed" and term_state != "completed":
560
+ console.print(f"\n[bold red]Failed:[/bold red] Session {sid} finished but reached non-completed terminal state '{term_state}'")
561
+ raise typer.Exit(1)
562
+ elif until == "terminal":
563
+ console.print(f"\n[bold green]Terminal State Reached:[/bold green] Session {sid} reached state '{term_state}'.")
564
+ if term_state == "completed":
565
+ raise typer.Exit(0)
566
+ else:
567
+ raise typer.Exit(1)
568
+
569
+ time_to_wait = min(interval, timeout_seconds - elapsed) if timeout_seconds > 0 else interval
570
+ if time_to_wait <= 0:
571
+ continue
572
+ time.sleep(time_to_wait)
573
+ except KeyboardInterrupt:
574
+ console.print("\n[dim]Wait interrupted.[/dim]")
575
+ raise typer.Exit(130)
576
+
577
+
459
578
  # --- Knowledge ---
460
579
  @knowledge_app.command("list")
461
580
  @handle_api_error
@@ -694,19 +813,51 @@ def create_schedule_cmd(
694
813
  def list_repos_cmd(
695
814
  org: Optional[str] = typer.Option(None, "--org"),
696
815
  json_output: bool = typer.Option(False, "--json"),
816
+ after: Optional[str] = typer.Option(None, "--after", help="Cursor for pagination"),
817
+ all_pages: bool = typer.Option(False, "--all", help="Fetch all pages sequentially"),
697
818
  ):
698
819
  """List repositories indexed for Devin."""
699
820
  if org: config.temporary_org_id = org
700
- resp = repositories.list_repositories()
701
- if isinstance(resp, list):
702
- items = resp
703
- elif isinstance(resp, dict):
704
- items = resp.get("repositories") or resp.get("items") or resp.get("data") or []
705
- else:
706
- items = []
707
- if json_output:
708
- console.print(json.dumps(items, indent=2))
709
- return
821
+
822
+ items = []
823
+ current_after = after
824
+ has_next = False
825
+ next_cursor = None
826
+
827
+ while True:
828
+ resp = repositories.list_repositories(after=current_after)
829
+ page_items = []
830
+ if isinstance(resp, list):
831
+ page_items = resp
832
+ has_next = False
833
+ next_cursor = None
834
+ elif isinstance(resp, dict):
835
+ page_items = resp.get("repositories") or resp.get("items") or resp.get("data") or []
836
+ has_next = resp.get("has_next_page", False)
837
+ next_cursor = resp.get("end_cursor") or resp.get("next_cursor")
838
+ else:
839
+ page_items = []
840
+ has_next = False
841
+ next_cursor = None
842
+
843
+ items.extend(page_items)
844
+
845
+ if not all_pages or not has_next or not next_cursor:
846
+ break
847
+
848
+ current_after = next_cursor
849
+
850
+ final_resp = {
851
+ "repositories": items,
852
+ "end_cursor": next_cursor if not all_pages else None,
853
+ "has_next_page": has_next if not all_pages else False
854
+ }
855
+
856
+ if json_output or os.environ.get("DEVIN_OUTPUT_FORMAT") == "json":
857
+ if os.environ.get("DEVIN_OUTPUT_FORMAT") != "json":
858
+ print(json.dumps(final_resp, indent=2))
859
+ return final_resp
860
+
710
861
  table = Table(title="Repositories")
711
862
  table.add_column("Path", style="cyan")
712
863
  table.add_column("Indexed", style="green")
@@ -728,7 +879,160 @@ def list_repos_cmd(
728
879
  )
729
880
  table.add_row(path, "Yes" if indexed else "No")
730
881
  console.print(table)
731
- return resp
882
+
883
+ if has_next and not all_pages:
884
+ console.print(f"[dim]More pages available. Use --after {next_cursor} to fetch next page, or --all to fetch all.[/dim]")
885
+
886
+ return final_resp
887
+
888
+
889
+ @repo_app.command("status")
890
+ @handle_api_error
891
+ def status_repo_cmd(
892
+ paths: List[str] = typer.Argument(..., help="Repository paths (owner/repo)"),
893
+ org: Optional[str] = typer.Option(None, "--org"),
894
+ json_output: bool = typer.Option(False, "--json"),
895
+ ):
896
+ """Get indexing status for one or more repositories."""
897
+ if org: config.temporary_org_id = org
898
+
899
+ results = []
900
+ exit_code = 0
901
+
902
+ from concurrent.futures import ThreadPoolExecutor
903
+
904
+ def fetch_status(path):
905
+ try:
906
+ resp = repositories.get_indexing_status(path)
907
+ if isinstance(resp, dict):
908
+ if "repo_path" not in resp:
909
+ resp["repo_path"] = path
910
+ indexing_enabled = resp.get("indexing_enabled", False)
911
+ item_exit = 0 if indexing_enabled else 1
912
+ else:
913
+ resp = {"repo_path": path, "indexing_enabled": False, "raw": resp}
914
+ item_exit = 1
915
+ return (path, resp, item_exit, None)
916
+ except Exception as e:
917
+ return (path, None, None, e)
918
+
919
+ results_map = {}
920
+ errors_map = {}
921
+ exit_codes_map = {}
922
+
923
+ max_workers = min(10, len(paths))
924
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
925
+ futures = {executor.submit(fetch_status, path): path for path in paths}
926
+ for future in futures:
927
+ path = futures[future]
928
+ try:
929
+ p, resp, item_exit, err = future.result()
930
+ if err:
931
+ errors_map[path] = err
932
+ else:
933
+ results_map[path] = resp
934
+ exit_codes_map[path] = item_exit
935
+ except Exception as ex:
936
+ errors_map[path] = ex
937
+
938
+ for path in paths:
939
+ if path in errors_map:
940
+ e = errors_map[path]
941
+ if isinstance(e, APIError):
942
+ if e.status_code == 404:
943
+ resp = {
944
+ "repo_path": path,
945
+ "indexing_enabled": False,
946
+ "error": "Not registered in Devin (404)"
947
+ }
948
+ results.append(resp)
949
+ if len(paths) == 1:
950
+ exit_code = 2
951
+ else:
952
+ if len(paths) == 1:
953
+ is_json = os.environ.get("DEVIN_OUTPUT_FORMAT") == "json" or json_output
954
+ if is_json:
955
+ try:
956
+ err_json = e.response.json()
957
+ except Exception:
958
+ err_json = {"error": str(e)}
959
+ print(json.dumps(err_json, indent=2))
960
+ else:
961
+ console.print(f"[bold red]API Error:[/bold red] {e}")
962
+ raise typer.Exit(3)
963
+ else:
964
+ results.append({
965
+ "repo_path": path,
966
+ "indexing_enabled": False,
967
+ "error": str(e)
968
+ })
969
+ exit_code = 3
970
+ else:
971
+ if len(paths) == 1:
972
+ is_json = os.environ.get("DEVIN_OUTPUT_FORMAT") == "json" or json_output
973
+ if is_json:
974
+ print(json.dumps({"error": str(e)}, indent=2))
975
+ else:
976
+ console.print(f"[bold red]Error:[/bold red] {e}")
977
+ raise typer.Exit(3)
978
+ else:
979
+ results.append({
980
+ "repo_path": path,
981
+ "indexing_enabled": False,
982
+ "error": str(e)
983
+ })
984
+ exit_code = 3
985
+ else:
986
+ resp = results_map[path]
987
+ item_exit = exit_codes_map[path]
988
+ results.append(resp)
989
+ if len(paths) == 1:
990
+ exit_code = item_exit
991
+
992
+ if len(paths) == 1:
993
+ final_result = results[0]
994
+ else:
995
+ final_result = {"results": results}
996
+
997
+ if json_output or os.environ.get("DEVIN_OUTPUT_FORMAT") == "json":
998
+ if os.environ.get("DEVIN_OUTPUT_FORMAT") != "json":
999
+ print(json.dumps(final_result, indent=2))
1000
+ return final_result
1001
+
1002
+ if len(paths) == 1:
1003
+ res = results[0]
1004
+ if "error" in res:
1005
+ console.print(f"[bold red]Error:[/bold red] {res['error']}")
1006
+ else:
1007
+ console.print(Panel(json.dumps(res, indent=2), title=f"Status for {paths[0]}"))
1008
+ else:
1009
+ table = Table(title="Repository Indexing Status")
1010
+ table.add_column("Path", style="cyan")
1011
+ table.add_column("Indexing Enabled", style="magenta")
1012
+ table.add_column("Status / Details")
1013
+
1014
+ for res in results:
1015
+ path = res.get("repo_path", "")
1016
+ enabled = "Yes" if res.get("indexing_enabled") else "No"
1017
+
1018
+ if "error" in res:
1019
+ details = f"[red]{res['error']}[/red]"
1020
+ else:
1021
+ latest = res.get("latest_index") or {}
1022
+ status = latest.get("status", "unknown") if latest else "N/A"
1023
+ branch = latest.get("branch_name", "") if latest else ""
1024
+ details = f"Status: {status}" + (f" ({branch})" if branch else "")
1025
+
1026
+ table.add_row(path, enabled, details)
1027
+ console.print(table)
1028
+
1029
+ if len(paths) == 1:
1030
+ raise typer.Exit(exit_code)
1031
+ else:
1032
+ if exit_code >= 3:
1033
+ raise typer.Exit(exit_code)
1034
+ return final_result
1035
+
732
1036
 
733
1037
  @repo_app.command("index")
734
1038
  @handle_api_error
@@ -0,0 +1,121 @@
1
+ import pytest
2
+ from typer.testing import CliRunner
3
+ from unittest.mock import patch, MagicMock
4
+ from devin_cli.cli import app
5
+ from devin_cli.api.client import APIError
6
+
7
+ runner = CliRunner()
8
+
9
+ @patch("devin_cli.api.repositories.list_repositories")
10
+ def test_repos_list_pagination(mock_list):
11
+ # First call returns a page with has_next_page=True and end_cursor
12
+ # Second call returns a page with has_next_page=False
13
+ mock_list.side_effect = [
14
+ {
15
+ "repositories": [{"repo_path": "owner/repo1"}],
16
+ "has_next_page": True,
17
+ "end_cursor": "cursor123"
18
+ },
19
+ {
20
+ "repositories": [{"repo_path": "owner/repo2"}],
21
+ "has_next_page": False,
22
+ "end_cursor": None
23
+ }
24
+ ]
25
+
26
+ result = runner.invoke(app, ["repos", "list", "--all"])
27
+ assert result.exit_code == 0
28
+ assert "owner/repo1" in result.stdout
29
+ assert "owner/repo2" in result.stdout
30
+ assert mock_list.call_count == 2
31
+
32
+
33
+ @patch("devin_cli.api.repositories.get_indexing_status")
34
+ def test_repos_status_enabled(mock_status):
35
+ mock_status.return_value = {
36
+ "repo_path": "owner/repo",
37
+ "indexing_enabled": True,
38
+ "latest_index": {"status": "completed"}
39
+ }
40
+
41
+ result = runner.invoke(app, ["repos", "status", "owner/repo"])
42
+ assert result.exit_code == 0
43
+ assert "indexing_enabled" in result.stdout
44
+
45
+
46
+ @patch("devin_cli.api.repositories.get_indexing_status")
47
+ def test_repos_status_disabled(mock_status):
48
+ mock_status.return_value = {
49
+ "repo_path": "owner/repo",
50
+ "indexing_enabled": False
51
+ }
52
+
53
+ result = runner.invoke(app, ["repos", "status", "owner/repo"])
54
+ assert result.exit_code == 1
55
+
56
+
57
+ @patch("devin_cli.api.repositories.get_indexing_status")
58
+ def test_repos_status_not_registered(mock_status):
59
+ # Raise a 404 APIError
60
+ mock_status.side_effect = APIError("Not found", 404)
61
+
62
+ result = runner.invoke(app, ["repos", "status", "owner/repo"])
63
+ assert result.exit_code == 2
64
+ assert "Not registered in Devin" in result.stdout
65
+
66
+
67
+ @patch("devin_cli.api.repositories.get_indexing_status")
68
+ def test_repos_status_multi(mock_status):
69
+ mock_status.side_effect = [
70
+ {"repo_path": "owner/repo1", "indexing_enabled": True},
71
+ APIError("Not found", 404),
72
+ ]
73
+
74
+ result = runner.invoke(app, ["repos", "status", "owner/repo1", "owner/repo2"])
75
+ assert result.exit_code == 0 # Multi-path doesn't fail exit code on 404
76
+ assert "Repository Indexing Status" in result.stdout
77
+ assert "owner/repo1" in result.stdout
78
+ assert "owner/repo2" in result.stdout
79
+
80
+
81
+ @patch("devin_cli.api.sessions.get_session")
82
+ def test_sessions_get_terminal_state(mock_get):
83
+ mock_get.return_value = {
84
+ "session_id": "sess_123",
85
+ "status_enum": "finished",
86
+ "title": "Test"
87
+ }
88
+
89
+ result = runner.invoke(app, ["sessions", "get", "sess_123"])
90
+ assert result.exit_code == 0
91
+ assert '"terminal_state": "completed"' in result.stdout
92
+
93
+
94
+ @patch("devin_cli.api.sessions.get_session")
95
+ def test_sessions_wait(mock_get):
96
+ mock_get.side_effect = [
97
+ {"session_id": "sess_123", "status_enum": "running"},
98
+ {"session_id": "sess_123", "status_enum": "finished"}
99
+ ]
100
+
101
+ result = runner.invoke(app, ["sessions", "wait", "sess_123", "--interval", "0"])
102
+ assert result.exit_code == 0
103
+ assert "completed successfully" in result.stdout
104
+ assert mock_get.call_count == 2
105
+
106
+
107
+ @patch("devin_cli.api.repositories.get_indexing_status")
108
+ def test_repos_status_other_error(mock_status):
109
+ # Raise a non-404 APIError (e.g., 403 Forbidden)
110
+ mock_status.side_effect = APIError("Forbidden", 403)
111
+
112
+ result = runner.invoke(app, ["repos", "status", "owner/repo"])
113
+ assert result.exit_code == 3
114
+ assert "API Error" in result.stdout or "Forbidden" in result.stdout
115
+
116
+
117
+ def test_sessions_wait_negative_timeout():
118
+ result = runner.invoke(app, ["sessions", "wait", "sess_123", "--timeout", "-10s"])
119
+ assert result.exit_code == 1
120
+ assert "cannot be negative" in result.stdout
121
+
@@ -1 +0,0 @@
1
- __version__ = "0.1.0"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes