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.
- {devin_cli-1.3.4 → devin_cli-1.3.5}/PKG-INFO +1 -1
- {devin_cli-1.3.4 → devin_cli-1.3.5}/pyproject.toml +1 -1
- devin_cli-1.3.5/src/devin_cli/__init__.py +1 -0
- {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/cli.py +316 -12
- devin_cli-1.3.5/tests/test_cli_extended.py +121 -0
- devin_cli-1.3.4/src/devin_cli/__init__.py +0 -1
- {devin_cli-1.3.4 → devin_cli-1.3.5}/.github/workflows/pypi-publish.yml +0 -0
- {devin_cli-1.3.4 → devin_cli-1.3.5}/.gitignore +0 -0
- {devin_cli-1.3.4 → devin_cli-1.3.5}/CHANGELOG.md +0 -0
- {devin_cli-1.3.4 → devin_cli-1.3.5}/LICENSE +0 -0
- {devin_cli-1.3.4 → devin_cli-1.3.5}/README.md +0 -0
- {devin_cli-1.3.4 → devin_cli-1.3.5}/assets/logo.png +0 -0
- {devin_cli-1.3.4 → devin_cli-1.3.5}/generate_facades.py +0 -0
- {devin_cli-1.3.4 → devin_cli-1.3.5}/setup.py +0 -0
- {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/__init__.py +0 -0
- {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/attachments.py +0 -0
- {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/client.py +0 -0
- {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/consumption.py +0 -0
- {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/knowledge.py +0 -0
- {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/members.py +0 -0
- {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/organizations.py +0 -0
- {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/playbooks.py +0 -0
- {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/repositories.py +0 -0
- {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/schedules.py +0 -0
- {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/secrets.py +0 -0
- {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/sessions.py +0 -0
- {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/v1/__init__.py +0 -0
- {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/v1/attachments.py +0 -0
- {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/v1/knowledge.py +0 -0
- {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/v1/playbooks.py +0 -0
- {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/v1/secrets.py +0 -0
- {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/v1/sessions.py +0 -0
- {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/v3/__init__.py +0 -0
- {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/v3/attachments.py +0 -0
- {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/v3/consumption.py +0 -0
- {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/v3/knowledge.py +0 -0
- {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/v3/members.py +0 -0
- {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/v3/organizations.py +0 -0
- {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/v3/playbooks.py +0 -0
- {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/v3/repositories.py +0 -0
- {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/v3/schedules.py +0 -0
- {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/v3/secrets.py +0 -0
- {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/api/v3/sessions.py +0 -0
- {devin_cli-1.3.4 → devin_cli-1.3.5}/src/devin_cli/config.py +0 -0
- {devin_cli-1.3.4 → devin_cli-1.3.5}/tests/__init__.py +0 -0
- {devin_cli-1.3.4 → devin_cli-1.3.5}/tests/conftest.py +0 -0
- {devin_cli-1.3.4 → devin_cli-1.3.5}/tests/test_api/test_attachments.py +0 -0
- {devin_cli-1.3.4 → devin_cli-1.3.5}/tests/test_api/test_knowledge.py +0 -0
- {devin_cli-1.3.4 → devin_cli-1.3.5}/tests/test_api/test_playbooks.py +0 -0
- {devin_cli-1.3.4 → devin_cli-1.3.5}/tests/test_api/test_repositories.py +0 -0
- {devin_cli-1.3.4 → devin_cli-1.3.5}/tests/test_api/test_schedules.py +0 -0
- {devin_cli-1.3.4 → devin_cli-1.3.5}/tests/test_api/test_secrets.py +0 -0
- {devin_cli-1.3.4 → devin_cli-1.3.5}/tests/test_api/test_sessions.py +0 -0
- {devin_cli-1.3.4 → devin_cli-1.3.5}/tests/test_cli.py +0 -0
- {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.
|
|
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
|
|
@@ -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(
|
|
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
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|