devs-cli 5.0.5__tar.gz → 6.0.0__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.
- {devs_cli-5.0.5/devs_cli.egg-info → devs_cli-6.0.0}/PKG-INFO +1 -1
- {devs_cli-5.0.5 → devs_cli-6.0.0}/devs/cli.py +145 -170
- {devs_cli-5.0.5 → devs_cli-6.0.0/devs_cli.egg-info}/PKG-INFO +1 -1
- {devs_cli-5.0.5 → devs_cli-6.0.0}/devs_cli.egg-info/SOURCES.txt +1 -0
- {devs_cli-5.0.5 → devs_cli-6.0.0}/pyproject.toml +1 -1
- devs_cli-6.0.0/tests/test_agents.py +45 -0
- {devs_cli-5.0.5 → devs_cli-6.0.0}/tests/test_cli.py +33 -9
- {devs_cli-5.0.5 → devs_cli-6.0.0}/tests/test_cli_misc.py +5 -5
- {devs_cli-5.0.5 → devs_cli-6.0.0}/tests/test_cli_stop.py +27 -8
- {devs_cli-5.0.5 → devs_cli-6.0.0}/tests/test_cli_vscode.py +22 -13
- {devs_cli-5.0.5 → devs_cli-6.0.0}/tests/test_live_mode.py +8 -16
- {devs_cli-5.0.5 → devs_cli-6.0.0}/LICENSE +0 -0
- {devs_cli-5.0.5 → devs_cli-6.0.0}/README.md +0 -0
- {devs_cli-5.0.5 → devs_cli-6.0.0}/devs/__init__.py +0 -0
- {devs_cli-5.0.5 → devs_cli-6.0.0}/devs/config.py +0 -0
- {devs_cli-5.0.5 → devs_cli-6.0.0}/devs/core/__init__.py +0 -0
- {devs_cli-5.0.5 → devs_cli-6.0.0}/devs/core/integration.py +0 -0
- {devs_cli-5.0.5 → devs_cli-6.0.0}/devs/exceptions.py +0 -0
- {devs_cli-5.0.5 → devs_cli-6.0.0}/devs/utils/__init__.py +0 -0
- {devs_cli-5.0.5 → devs_cli-6.0.0}/devs_cli.egg-info/dependency_links.txt +0 -0
- {devs_cli-5.0.5 → devs_cli-6.0.0}/devs_cli.egg-info/entry_points.txt +0 -0
- {devs_cli-5.0.5 → devs_cli-6.0.0}/devs_cli.egg-info/requires.txt +0 -0
- {devs_cli-5.0.5 → devs_cli-6.0.0}/devs_cli.egg-info/top_level.txt +0 -0
- {devs_cli-5.0.5 → devs_cli-6.0.0}/setup.cfg +0 -0
- {devs_cli-5.0.5 → devs_cli-6.0.0}/tests/test_cli_clean.py +0 -0
- {devs_cli-5.0.5 → devs_cli-6.0.0}/tests/test_cli_start.py +0 -0
- {devs_cli-5.0.5 → devs_cli-6.0.0}/tests/test_container_manager.py +0 -0
- {devs_cli-5.0.5 → devs_cli-6.0.0}/tests/test_e2e.py +0 -0
- {devs_cli-5.0.5 → devs_cli-6.0.0}/tests/test_error_parsing.py +0 -0
- {devs_cli-5.0.5 → devs_cli-6.0.0}/tests/test_integration.py +0 -0
- {devs_cli-5.0.5 → devs_cli-6.0.0}/tests/test_project.py +0 -0
- {devs_cli-5.0.5 → devs_cli-6.0.0}/tests/test_repo_cache.py +0 -0
- {devs_cli-5.0.5 → devs_cli-6.0.0}/tests/test_workspace_manager.py +0 -0
|
@@ -5,6 +5,7 @@ import sys
|
|
|
5
5
|
import subprocess
|
|
6
6
|
import traceback
|
|
7
7
|
from functools import wraps
|
|
8
|
+
from typing import Callable, Sequence
|
|
8
9
|
from importlib.metadata import version, PackageNotFoundError
|
|
9
10
|
|
|
10
11
|
import click
|
|
@@ -14,6 +15,7 @@ from rich.table import Table
|
|
|
14
15
|
from .config import config
|
|
15
16
|
from .core import Project, ContainerManager, WorkspaceManager
|
|
16
17
|
from .core.integration import VSCodeIntegration, ExternalToolIntegration
|
|
18
|
+
from devs_common.agents import AgentSpec, get_agent
|
|
17
19
|
from devs_common.devs_config import DevsConfigLoader
|
|
18
20
|
from devs_common.utils.repo_cache import RepoCache
|
|
19
21
|
from .exceptions import (
|
|
@@ -379,9 +381,21 @@ def stop(dev_names: tuple) -> None:
|
|
|
379
381
|
|
|
380
382
|
container_manager = ContainerManager(project, config)
|
|
381
383
|
|
|
384
|
+
failed = []
|
|
382
385
|
for dev_name in dev_names:
|
|
383
386
|
console.print(f" Stopping: {dev_name}")
|
|
384
|
-
|
|
387
|
+
# Keep going past a failure so one bad container doesn't leave the rest running.
|
|
388
|
+
# stop_container reports Docker errors and missing containers itself, returning False.
|
|
389
|
+
try:
|
|
390
|
+
stopped = container_manager.stop_container(dev_name, remove=False)
|
|
391
|
+
except ContainerError as e:
|
|
392
|
+
console.print(f" ❌ Error stopping {dev_name}: {e}")
|
|
393
|
+
stopped = False
|
|
394
|
+
if stopped is False:
|
|
395
|
+
failed.append(dev_name)
|
|
396
|
+
|
|
397
|
+
if failed:
|
|
398
|
+
sys.exit(1)
|
|
385
399
|
|
|
386
400
|
|
|
387
401
|
@cli.command()
|
|
@@ -424,46 +438,8 @@ def shell(dev_name: str, live: bool, env: tuple, debug: bool) -> None:
|
|
|
424
438
|
sys.exit(1)
|
|
425
439
|
|
|
426
440
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
@click.argument('prompt', required=False)
|
|
430
|
-
@click.option('--auth', is_flag=True, help='Show Claude authentication setup instructions')
|
|
431
|
-
@click.option('--reset-workspace', is_flag=True, help='Reset workspace contents before execution')
|
|
432
|
-
@click.option('--live', is_flag=True, help='Start container with current directory mounted as workspace')
|
|
433
|
-
@click.option('--env', multiple=True, help='Environment variables to pass to container (format: VAR=value)')
|
|
434
|
-
@debug_option
|
|
435
|
-
def claude(dev_name: str, prompt: str, auth: bool, reset_workspace: bool, live: bool, env: tuple, debug: bool) -> None:
|
|
436
|
-
"""Execute Claude CLI in devcontainer or set up authentication.
|
|
437
|
-
|
|
438
|
-
DEV_NAME: Development environment name
|
|
439
|
-
PROMPT: Prompt to send to Claude
|
|
440
|
-
|
|
441
|
-
Example: devs claude sally "Summarize this codebase"
|
|
442
|
-
Example: devs claude sally "Fix the tests" --reset-workspace
|
|
443
|
-
Example: devs claude sally "Fix the tests" --live # Run with current directory
|
|
444
|
-
Example: devs claude sally "Start the server" --env QUART_PORT=5001
|
|
445
|
-
Example: devs claude --auth # Show auth setup instructions
|
|
446
|
-
"""
|
|
447
|
-
# Handle authentication mode
|
|
448
|
-
if auth:
|
|
449
|
-
console.print("🔐 Claude authentication for devcontainers")
|
|
450
|
-
console.print("")
|
|
451
|
-
console.print("1. Generate a token (on a machine with a browser):")
|
|
452
|
-
console.print(" [cyan]claude setup-token[/cyan]")
|
|
453
|
-
console.print("")
|
|
454
|
-
console.print("2. Add the token to your environment:")
|
|
455
|
-
console.print(" [cyan]export CLAUDE_CODE_OAUTH_TOKEN=<token>[/cyan]")
|
|
456
|
-
console.print("")
|
|
457
|
-
console.print(" Or add it to [cyan]~/.devs/envs/default/.env[/cyan]:")
|
|
458
|
-
console.print(" [dim]CLAUDE_CODE_OAUTH_TOKEN=<token>[/dim]")
|
|
459
|
-
console.print("")
|
|
460
|
-
console.print("The token will be automatically passed to all devcontainers.")
|
|
461
|
-
return
|
|
462
|
-
|
|
463
|
-
# Validate required arguments for execution mode
|
|
464
|
-
if not dev_name or not prompt:
|
|
465
|
-
raise click.UsageError("DEV_NAME and PROMPT are required unless using --auth")
|
|
466
|
-
|
|
441
|
+
def _run_agent(agent: AgentSpec, dev_name: str, prompt: str, reset_workspace: bool, live: bool, env: tuple, debug: bool) -> None:
|
|
442
|
+
"""Run a coding agent against a prompt in a devcontainer, streaming its output."""
|
|
467
443
|
check_dependencies()
|
|
468
444
|
project = get_project()
|
|
469
445
|
|
|
@@ -482,14 +458,15 @@ def claude(dev_name: str, prompt: str, auth: bool, reset_workspace: bool, live:
|
|
|
482
458
|
# Ensure workspace exists (handles live mode and reset internally)
|
|
483
459
|
workspace_dir = workspace_manager.create_workspace(dev_name, reset_contents=reset_workspace, live=live)
|
|
484
460
|
|
|
485
|
-
# Execute
|
|
486
|
-
console.print(f"🤖 Executing
|
|
461
|
+
# Execute the agent (ensure_container_running is called internally)
|
|
462
|
+
console.print(f"🤖 Executing {agent.display_name} in {dev_name}...")
|
|
487
463
|
if reset_workspace and not live:
|
|
488
464
|
console.print("🗑️ Workspace contents reset")
|
|
489
465
|
console.print(f"📝 Prompt: {prompt}")
|
|
490
466
|
console.print("")
|
|
491
467
|
|
|
492
|
-
success, output, error, _ = container_manager.
|
|
468
|
+
success, output, error, _ = container_manager.exec_agent(
|
|
469
|
+
agent,
|
|
493
470
|
dev_name=dev_name,
|
|
494
471
|
workspace_dir=workspace_dir,
|
|
495
472
|
prompt=prompt,
|
|
@@ -501,9 +478,9 @@ def claude(dev_name: str, prompt: str, auth: bool, reset_workspace: bool, live:
|
|
|
501
478
|
|
|
502
479
|
console.print("") # Add spacing after streamed output
|
|
503
480
|
if success:
|
|
504
|
-
console.print("✅
|
|
481
|
+
console.print(f"✅ {agent.display_name} execution completed")
|
|
505
482
|
else:
|
|
506
|
-
console.print("❌
|
|
483
|
+
console.print(f"❌ {agent.display_name} execution failed")
|
|
507
484
|
if error:
|
|
508
485
|
console.print("")
|
|
509
486
|
console.print("🚫 Error:")
|
|
@@ -511,98 +488,99 @@ def claude(dev_name: str, prompt: str, auth: bool, reset_workspace: bool, live:
|
|
|
511
488
|
sys.exit(1)
|
|
512
489
|
|
|
513
490
|
except (ContainerError, WorkspaceError) as e:
|
|
514
|
-
console.print(f"❌ Error executing
|
|
491
|
+
console.print(f"❌ Error executing {agent.display_name} in {dev_name}: {e}")
|
|
515
492
|
sys.exit(1)
|
|
516
493
|
|
|
517
494
|
|
|
495
|
+
def _add_agent_command(
|
|
496
|
+
name: str,
|
|
497
|
+
auth_help: str,
|
|
498
|
+
handle_auth: Callable[..., None],
|
|
499
|
+
auth_options: Sequence[Callable] = (),
|
|
500
|
+
auth_examples: Sequence[str] = (),
|
|
501
|
+
) -> None:
|
|
502
|
+
"""Register a `devs <agent> DEV_NAME PROMPT` command for an agent in devs_common.agents.
|
|
518
503
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
@cli.command()
|
|
527
|
-
@click.argument('dev_name', required=False)
|
|
528
|
-
@click.argument('prompt', required=False)
|
|
529
|
-
@click.option('--auth', is_flag=True, help='Set up Codex authentication for devcontainers')
|
|
530
|
-
@click.option('--api-key', help='OpenAI API key to authenticate with (use with --auth)')
|
|
531
|
-
@click.option('--reset-workspace', is_flag=True, help='Reset workspace contents before execution')
|
|
532
|
-
@click.option('--live', is_flag=True, help='Start container with current directory mounted as workspace')
|
|
533
|
-
@click.option('--env', multiple=True, help='Environment variables to pass to container (format: VAR=value)')
|
|
534
|
-
@debug_option
|
|
535
|
-
def codex(dev_name: str, prompt: str, auth: bool, api_key: str, reset_workspace: bool, live: bool, env: tuple, debug: bool) -> None:
|
|
536
|
-
"""Execute OpenAI Codex CLI in devcontainer or set up authentication.
|
|
537
|
-
|
|
538
|
-
DEV_NAME: Development environment name
|
|
539
|
-
PROMPT: Prompt to send to Codex
|
|
540
|
-
|
|
541
|
-
Example: devs codex sally "Summarize this codebase"
|
|
542
|
-
Example: devs codex sally "Fix the tests" --reset-workspace
|
|
543
|
-
Example: devs codex sally "Fix the tests" --live # Run with current directory
|
|
544
|
-
Example: devs codex sally "Start the server" --env QUART_PORT=5001
|
|
545
|
-
Example: devs codex --auth # Interactive authentication
|
|
546
|
-
Example: devs codex --auth --api-key <YOUR_KEY> # API key authentication
|
|
504
|
+
Args:
|
|
505
|
+
name: Agent name in the AGENTS registry
|
|
506
|
+
auth_help: Help text for the --auth flag
|
|
507
|
+
handle_auth: Called for `--auth` with debug= plus any auth_options values
|
|
508
|
+
auth_options: Extra click options that only apply to --auth (e.g. --api-key)
|
|
509
|
+
auth_examples: Extra `devs <agent> --auth ...` example lines for the help text
|
|
547
510
|
"""
|
|
548
|
-
|
|
549
|
-
if auth:
|
|
550
|
-
_handle_codex_auth(api_key=api_key, debug=debug)
|
|
551
|
-
return
|
|
552
|
-
|
|
553
|
-
# Validate required arguments for execution mode
|
|
554
|
-
if not dev_name or not prompt:
|
|
555
|
-
raise click.UsageError("DEV_NAME and PROMPT are required unless using --auth")
|
|
556
|
-
|
|
557
|
-
check_dependencies()
|
|
558
|
-
project = get_project()
|
|
559
|
-
|
|
560
|
-
# Load environment variables from DEVS.yml and merge with CLI --env flags
|
|
561
|
-
devs_env = DevsConfigLoader.load_env_vars(dev_name, project.info.name)
|
|
562
|
-
cli_env = parse_env_vars(env) if env else {}
|
|
563
|
-
extra_env = merge_env_vars(devs_env, cli_env) if devs_env or cli_env else None
|
|
564
|
-
|
|
565
|
-
if extra_env:
|
|
566
|
-
console.print(f"🔧 Environment variables: {', '.join(f'{k}={v}' for k, v in extra_env.items())}")
|
|
567
|
-
|
|
568
|
-
container_manager = ContainerManager(project, config)
|
|
569
|
-
workspace_manager = WorkspaceManager(project, config)
|
|
511
|
+
agent = get_agent(name)
|
|
570
512
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
if
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
513
|
+
def command(dev_name: str, prompt: str, auth: bool, reset_workspace: bool, live: bool,
|
|
514
|
+
env: tuple, debug: bool, **auth_values) -> None:
|
|
515
|
+
if auth:
|
|
516
|
+
handle_auth(debug=debug, **auth_values)
|
|
517
|
+
return
|
|
518
|
+
# Validate required arguments for execution mode
|
|
519
|
+
if not dev_name or not prompt:
|
|
520
|
+
raise click.UsageError("DEV_NAME and PROMPT are required unless using --auth")
|
|
521
|
+
_run_agent(agent, dev_name, prompt, reset_workspace, live, env, debug)
|
|
522
|
+
|
|
523
|
+
examples = [
|
|
524
|
+
f'devs {name} sally "Summarize this codebase"',
|
|
525
|
+
f'devs {name} sally "Fix the tests" --reset-workspace',
|
|
526
|
+
f'devs {name} sally "Fix the tests" --live # Run with current directory',
|
|
527
|
+
f'devs {name} sally "Start the server" --env QUART_PORT=5001',
|
|
528
|
+
f'devs {name} --auth',
|
|
529
|
+
*auth_examples,
|
|
530
|
+
]
|
|
531
|
+
command.__name__ = name
|
|
532
|
+
command.__doc__ = (
|
|
533
|
+
f"Execute {agent.description} in devcontainer or set up authentication.\n\n"
|
|
534
|
+
f"DEV_NAME: Development environment name\n\n"
|
|
535
|
+
f"PROMPT: Prompt to send to {agent.display_name}\n\n"
|
|
536
|
+
+ "\n\n".join(f"Example: {example}" for example in examples)
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
decorators = [
|
|
540
|
+
cli.command(name=name),
|
|
541
|
+
click.argument('dev_name', required=False),
|
|
542
|
+
click.argument('prompt', required=False),
|
|
543
|
+
click.option('--auth', is_flag=True, help=auth_help),
|
|
544
|
+
*auth_options,
|
|
545
|
+
click.option('--reset-workspace', is_flag=True, help='Reset workspace contents before execution'),
|
|
546
|
+
click.option('--live', is_flag=True, help='Start container with current directory mounted as workspace'),
|
|
547
|
+
click.option('--env', multiple=True, help='Environment variables to pass to container (format: VAR=value)'),
|
|
548
|
+
debug_option,
|
|
549
|
+
]
|
|
550
|
+
for decorator in reversed(decorators):
|
|
551
|
+
command = decorator(command)
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def _show_claude_auth(debug: bool) -> None:
|
|
555
|
+
"""Explain how to give devcontainers a Claude token."""
|
|
556
|
+
console.print("🔐 Claude authentication for devcontainers")
|
|
557
|
+
console.print("")
|
|
558
|
+
console.print("1. Generate a token (on a machine with a browser):")
|
|
559
|
+
console.print(" [cyan]claude setup-token[/cyan]")
|
|
560
|
+
console.print("")
|
|
561
|
+
console.print("2. Add the token to your environment:")
|
|
562
|
+
console.print(" [cyan]export CLAUDE_CODE_OAUTH_TOKEN=<token>[/cyan]")
|
|
563
|
+
console.print("")
|
|
564
|
+
console.print(" Or add it to [cyan]~/.devs/envs/default/.env[/cyan]:")
|
|
565
|
+
console.print(" [dim]CLAUDE_CODE_OAUTH_TOKEN=<token>[/dim]")
|
|
566
|
+
console.print("")
|
|
567
|
+
console.print("The token will be automatically passed to all devcontainers.")
|
|
591
568
|
|
|
592
|
-
console.print("") # Add spacing after streamed output
|
|
593
|
-
if success:
|
|
594
|
-
console.print("✅ Codex execution completed")
|
|
595
|
-
else:
|
|
596
|
-
console.print("❌ Codex execution failed")
|
|
597
|
-
if error:
|
|
598
|
-
console.print("")
|
|
599
|
-
console.print("🚫 Error:")
|
|
600
|
-
console.print(error)
|
|
601
|
-
sys.exit(1)
|
|
602
569
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
570
|
+
def _show_hermes_auth(debug: bool) -> None:
|
|
571
|
+
"""Explain how to give devcontainers an OpenRouter key for Hermes."""
|
|
572
|
+
console.print("🔐 Hermes Agent authentication for devcontainers")
|
|
573
|
+
console.print("")
|
|
574
|
+
console.print("Hermes runs against OpenRouter. Create an API key at:")
|
|
575
|
+
console.print(" [cyan]https://openrouter.ai/keys[/cyan]")
|
|
576
|
+
console.print("")
|
|
577
|
+
console.print("Add it to [cyan]~/.devs/envs/default/.env[/cyan] (or a project-specific env dir):")
|
|
578
|
+
console.print(" [dim]OPENROUTER_API_KEY=<key>[/dim]")
|
|
579
|
+
console.print("")
|
|
580
|
+
console.print("The default model is [cyan]z-ai/glm-5.3[/cyan]. To use another OpenRouter model:")
|
|
581
|
+
console.print(" [dim]HERMES_INFERENCE_MODEL=<provider/model>[/dim]")
|
|
582
|
+
console.print("")
|
|
583
|
+
console.print("The env file is mounted into every devcontainer and loaded on each shell.")
|
|
606
584
|
|
|
607
585
|
|
|
608
586
|
def _handle_codex_auth(api_key: str, debug: bool) -> None:
|
|
@@ -619,54 +597,32 @@ def _handle_codex_auth(api_key: str, debug: bool) -> None:
|
|
|
619
597
|
console.print("🔐 Setting up Codex authentication...")
|
|
620
598
|
console.print(f" Configuration will be saved to: {config.codex_config_dir}")
|
|
621
599
|
|
|
600
|
+
# Codex reads its state directory from CODEX_HOME (default ~/.codex); pointing it
|
|
601
|
+
# at the directory that is bind-mounted into containers shares the login with them.
|
|
602
|
+
env = os.environ.copy()
|
|
603
|
+
env['CODEX_HOME'] = str(config.codex_config_dir)
|
|
604
|
+
|
|
622
605
|
if api_key:
|
|
623
|
-
# Set API key directly using Codex CLI
|
|
624
606
|
console.print(" Using provided API key...")
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
env = os.environ.copy()
|
|
628
|
-
env['CODEX_CONFIG_HOME'] = str(config.codex_config_dir)
|
|
629
|
-
|
|
630
|
-
cmd = ['codex', 'auth', '--api-key', api_key]
|
|
631
|
-
|
|
632
|
-
if debug:
|
|
633
|
-
console.print(f"[dim]Running: {' '.join(cmd)}[/dim]")
|
|
634
|
-
console.print(f"[dim]CODEX_CONFIG_HOME: {config.codex_config_dir}[/dim]")
|
|
635
|
-
|
|
636
|
-
result = subprocess.run(
|
|
637
|
-
cmd,
|
|
638
|
-
env=env,
|
|
639
|
-
capture_output=True,
|
|
640
|
-
text=True
|
|
641
|
-
)
|
|
642
|
-
|
|
643
|
-
if result.returncode != 0:
|
|
644
|
-
error_msg = result.stderr or result.stdout or "Unknown error"
|
|
645
|
-
raise Exception(f"Codex authentication failed: {error_msg}")
|
|
646
|
-
|
|
607
|
+
# The key goes in on stdin so it never shows up in the process list
|
|
608
|
+
cmd = ['codex', 'login', '--with-api-key']
|
|
647
609
|
else:
|
|
648
|
-
# Interactive authentication
|
|
649
610
|
console.print(" Starting interactive authentication...")
|
|
650
611
|
console.print(" Follow the prompts to authenticate with Codex")
|
|
651
612
|
console.print("")
|
|
613
|
+
cmd = ['codex', 'login']
|
|
652
614
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
cmd = ['codex', 'auth']
|
|
658
|
-
|
|
659
|
-
if debug:
|
|
660
|
-
console.print(f"[dim]Running: {' '.join(cmd)}[/dim]")
|
|
661
|
-
console.print(f"[dim]CODEX_CONFIG_HOME: {config.codex_config_dir}[/dim]")
|
|
662
|
-
|
|
663
|
-
# Run interactively
|
|
664
|
-
result = subprocess.run(
|
|
665
|
-
cmd,
|
|
666
|
-
env=env,
|
|
667
|
-
check=False
|
|
668
|
-
)
|
|
615
|
+
if debug:
|
|
616
|
+
console.print(f"[dim]Running: {' '.join(cmd)}[/dim]")
|
|
617
|
+
console.print(f"[dim]CODEX_HOME: {config.codex_config_dir}[/dim]")
|
|
669
618
|
|
|
619
|
+
if api_key:
|
|
620
|
+
result = subprocess.run(cmd, env=env, input=api_key, capture_output=True, text=True)
|
|
621
|
+
if result.returncode != 0:
|
|
622
|
+
error_msg = result.stderr or result.stdout or "Unknown error"
|
|
623
|
+
raise Exception(f"Codex authentication failed: {error_msg}")
|
|
624
|
+
else:
|
|
625
|
+
result = subprocess.run(cmd, env=env, check=False)
|
|
670
626
|
if result.returncode != 0:
|
|
671
627
|
raise Exception("Codex authentication was cancelled or failed")
|
|
672
628
|
|
|
@@ -695,6 +651,25 @@ def _handle_codex_auth(api_key: str, debug: bool) -> None:
|
|
|
695
651
|
sys.exit(1)
|
|
696
652
|
|
|
697
653
|
|
|
654
|
+
_add_agent_command(
|
|
655
|
+
"claude",
|
|
656
|
+
auth_help='Show Claude authentication setup instructions',
|
|
657
|
+
handle_auth=_show_claude_auth,
|
|
658
|
+
)
|
|
659
|
+
_add_agent_command(
|
|
660
|
+
"codex",
|
|
661
|
+
auth_help='Set up Codex authentication for devcontainers',
|
|
662
|
+
handle_auth=_handle_codex_auth,
|
|
663
|
+
auth_options=[click.option('--api-key', help='OpenAI API key to authenticate with (use with --auth)')],
|
|
664
|
+
auth_examples=['devs codex --auth --api-key <YOUR_KEY> # API key authentication'],
|
|
665
|
+
)
|
|
666
|
+
_add_agent_command(
|
|
667
|
+
"hermes",
|
|
668
|
+
auth_help='Show Hermes (OpenRouter) authentication setup instructions',
|
|
669
|
+
handle_auth=_show_hermes_auth,
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
|
|
698
673
|
@cli.command()
|
|
699
674
|
@click.argument('dev_name')
|
|
700
675
|
@click.option('--reset-workspace', is_flag=True, help='Reset workspace contents before execution')
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Tests for the coding agent registry and the commands generated from it."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import patch
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from click.testing import CliRunner
|
|
7
|
+
|
|
8
|
+
from devs.cli import cli
|
|
9
|
+
from devs_common.agents import AGENTS, get_agent
|
|
10
|
+
from devs_common.core.container import ContainerManager
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_every_registered_agent_has_a_cli_command():
|
|
14
|
+
"""Each AgentSpec gets a `devs <name>` command."""
|
|
15
|
+
for name in AGENTS:
|
|
16
|
+
assert name in cli.commands, f"missing `devs {name}` command"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.mark.parametrize("name", list(AGENTS))
|
|
20
|
+
def test_agent_command_help_uses_spec(name):
|
|
21
|
+
result = CliRunner().invoke(cli, [name, '--help'])
|
|
22
|
+
|
|
23
|
+
assert result.exit_code == 0
|
|
24
|
+
assert f"Execute {AGENTS[name].description} in devcontainer" in result.output
|
|
25
|
+
assert f"devs {name} sally" in result.output
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_get_agent_unknown_name():
|
|
29
|
+
with pytest.raises(ValueError, match="Unknown agent 'nope'"):
|
|
30
|
+
get_agent('nope')
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@pytest.mark.parametrize("name", list(AGENTS))
|
|
34
|
+
def test_exec_agent_runs_spec_command_with_prompt_on_stdin(name, tmp_path):
|
|
35
|
+
# exec_agent only delegates to exec_command, so skip __init__ (which needs Docker)
|
|
36
|
+
manager = ContainerManager.__new__(ContainerManager)
|
|
37
|
+
with patch.object(manager, 'exec_command', return_value=(True, "ok", "", 0)) as exec_command:
|
|
38
|
+
result = manager.exec_agent(name, dev_name='sally', workspace_dir=tmp_path,
|
|
39
|
+
prompt='Fix the tests', live=True)
|
|
40
|
+
|
|
41
|
+
assert result == (True, "ok", "", 0)
|
|
42
|
+
kwargs = exec_command.call_args.kwargs
|
|
43
|
+
assert kwargs['command'] == AGENTS[name].command
|
|
44
|
+
assert kwargs['stdin_input'] == 'Fix the tests'
|
|
45
|
+
assert kwargs['live'] is True
|
|
@@ -29,7 +29,7 @@ class TestCLI:
|
|
|
29
29
|
result = runner.invoke(cli, ['--version'])
|
|
30
30
|
|
|
31
31
|
assert result.exit_code == 0
|
|
32
|
-
assert "
|
|
32
|
+
assert "devs-cli" in result.output
|
|
33
33
|
|
|
34
34
|
def test_start_command_help(self):
|
|
35
35
|
"""Test start command help."""
|
|
@@ -55,7 +55,7 @@ class TestCLI:
|
|
|
55
55
|
result = runner.invoke(cli, ['stop', '--help'])
|
|
56
56
|
|
|
57
57
|
assert result.exit_code == 0
|
|
58
|
-
assert "Stop
|
|
58
|
+
assert "Stop devcontainers" in result.output
|
|
59
59
|
assert "DEV_NAMES" in result.output
|
|
60
60
|
|
|
61
61
|
def test_shell_command_help(self):
|
|
@@ -178,10 +178,10 @@ class TestCLI:
|
|
|
178
178
|
# Verify subprocess was called with correct arguments
|
|
179
179
|
mock_subprocess.assert_called_once()
|
|
180
180
|
call_args = mock_subprocess.call_args
|
|
181
|
-
assert
|
|
182
|
-
|
|
183
|
-
assert '
|
|
184
|
-
assert 'test-
|
|
181
|
+
assert call_args[0][0] == ['codex', 'login', '--with-api-key']
|
|
182
|
+
# Key is passed on stdin, not argv, and login state goes to the mounted dir
|
|
183
|
+
assert call_args.kwargs['input'] == 'test-key-123'
|
|
184
|
+
assert call_args.kwargs['env']['CODEX_HOME'] == '/tmp/test-codex-config'
|
|
185
185
|
|
|
186
186
|
@patch('devs.cli.subprocess.run')
|
|
187
187
|
@patch('devs.cli.config')
|
|
@@ -203,9 +203,8 @@ class TestCLI:
|
|
|
203
203
|
# Verify subprocess was called for interactive auth
|
|
204
204
|
mock_subprocess.assert_called_once()
|
|
205
205
|
call_args = mock_subprocess.call_args
|
|
206
|
-
assert
|
|
207
|
-
assert '
|
|
208
|
-
assert '--api-key' not in call_args[0][0]
|
|
206
|
+
assert call_args[0][0] == ['codex', 'login']
|
|
207
|
+
assert call_args.kwargs['env']['CODEX_HOME'] == '/tmp/test-codex-config'
|
|
209
208
|
|
|
210
209
|
@patch('devs.cli.subprocess.run')
|
|
211
210
|
@patch('devs.cli.config')
|
|
@@ -223,6 +222,31 @@ class TestCLI:
|
|
|
223
222
|
assert "Codex CLI not found" in result.output
|
|
224
223
|
assert "npm install -g @openai/codex" in result.output
|
|
225
224
|
|
|
225
|
+
def test_hermes_command_help(self):
|
|
226
|
+
"""Test hermes command help."""
|
|
227
|
+
runner = CliRunner()
|
|
228
|
+
result = runner.invoke(cli, ['hermes', '--help'])
|
|
229
|
+
|
|
230
|
+
assert result.exit_code == 0
|
|
231
|
+
assert "Execute Hermes Agent" in result.output
|
|
232
|
+
assert "--auth" in result.output
|
|
233
|
+
|
|
234
|
+
def test_hermes_auth_shows_instructions(self):
|
|
235
|
+
"""Test hermes --auth shows OpenRouter setup instructions."""
|
|
236
|
+
runner = CliRunner()
|
|
237
|
+
result = runner.invoke(cli, ['hermes', '--auth'])
|
|
238
|
+
|
|
239
|
+
assert result.exit_code == 0
|
|
240
|
+
assert "OPENROUTER_API_KEY" in result.output
|
|
241
|
+
|
|
242
|
+
def test_hermes_missing_args(self):
|
|
243
|
+
"""Test hermes command without required args (not using --auth)."""
|
|
244
|
+
runner = CliRunner()
|
|
245
|
+
result = runner.invoke(cli, ['hermes'])
|
|
246
|
+
|
|
247
|
+
assert result.exit_code != 0
|
|
248
|
+
assert "DEV_NAME and PROMPT are required unless using --auth" in result.output
|
|
249
|
+
|
|
226
250
|
def test_codex_missing_args(self):
|
|
227
251
|
"""Test codex command without required args (not using --auth)."""
|
|
228
252
|
runner = CliRunner()
|
|
@@ -288,7 +288,7 @@ class TestShellCommand:
|
|
|
288
288
|
|
|
289
289
|
# Container manager raises error
|
|
290
290
|
mock_container_manager = Mock()
|
|
291
|
-
mock_container_manager.
|
|
291
|
+
mock_container_manager.exec_shell.side_effect = ContainerError("Container failed")
|
|
292
292
|
mock_container_manager_class.return_value = mock_container_manager
|
|
293
293
|
|
|
294
294
|
# Run command
|
|
@@ -319,7 +319,7 @@ class TestClaudeCommand:
|
|
|
319
319
|
|
|
320
320
|
mock_container_manager = Mock()
|
|
321
321
|
mock_container_manager.ensure_container_running.return_value = True
|
|
322
|
-
mock_container_manager.
|
|
322
|
+
mock_container_manager.exec_agent.return_value = (True, "Claude response", None, None)
|
|
323
323
|
mock_container_manager_class.return_value = mock_container_manager
|
|
324
324
|
|
|
325
325
|
# Run command
|
|
@@ -328,7 +328,7 @@ class TestClaudeCommand:
|
|
|
328
328
|
# Verify
|
|
329
329
|
assert result.exit_code == 0
|
|
330
330
|
assert "Executing Claude in alice" in result.output
|
|
331
|
-
mock_container_manager.
|
|
331
|
+
mock_container_manager.exec_agent.assert_called_once()
|
|
332
332
|
|
|
333
333
|
@patch('devs.cli.get_project')
|
|
334
334
|
@patch('devs.cli.WorkspaceManager')
|
|
@@ -349,7 +349,7 @@ class TestClaudeCommand:
|
|
|
349
349
|
|
|
350
350
|
# Container manager raises error
|
|
351
351
|
mock_container_manager = Mock()
|
|
352
|
-
mock_container_manager.
|
|
352
|
+
mock_container_manager.exec_agent.side_effect = ContainerError("Container failed")
|
|
353
353
|
mock_container_manager_class.return_value = mock_container_manager
|
|
354
354
|
|
|
355
355
|
# Run command
|
|
@@ -376,7 +376,7 @@ class TestClaudeCommand:
|
|
|
376
376
|
|
|
377
377
|
mock_container_manager = Mock()
|
|
378
378
|
mock_container_manager.ensure_container_running.return_value = True
|
|
379
|
-
mock_container_manager.
|
|
379
|
+
mock_container_manager.exec_agent.return_value = (True, "Claude response", None, None)
|
|
380
380
|
mock_container_manager_class.return_value = mock_container_manager
|
|
381
381
|
|
|
382
382
|
# Run command with env vars
|
|
@@ -11,6 +11,24 @@ from devs.cli import cli
|
|
|
11
11
|
class TestStopCommand:
|
|
12
12
|
"""Test suite for 'devs stop' command."""
|
|
13
13
|
|
|
14
|
+
@patch('devs.cli.get_project')
|
|
15
|
+
@patch('devs.cli.ContainerManager')
|
|
16
|
+
def test_stop_exits_nonzero_when_stop_fails(self, mock_container_manager_class, mock_get_project,
|
|
17
|
+
cli_runner, temp_project):
|
|
18
|
+
"""stop_container returns False (container missing or Docker error) -> exit 1."""
|
|
19
|
+
mock_project = Mock()
|
|
20
|
+
mock_project.info.name = "test-org-test-repo"
|
|
21
|
+
mock_get_project.return_value = mock_project
|
|
22
|
+
|
|
23
|
+
mock_container_manager = Mock()
|
|
24
|
+
mock_container_manager.stop_container.side_effect = [False, True]
|
|
25
|
+
mock_container_manager_class.return_value = mock_container_manager
|
|
26
|
+
|
|
27
|
+
result = cli_runner.invoke(cli, ['stop', 'alice', 'bob'])
|
|
28
|
+
|
|
29
|
+
assert result.exit_code == 1
|
|
30
|
+
assert mock_container_manager.stop_container.call_count == 2
|
|
31
|
+
|
|
14
32
|
@patch('devs.cli.get_project')
|
|
15
33
|
@patch('devs.cli.ContainerManager')
|
|
16
34
|
def test_stop_single_container(self, mock_container_manager_class, mock_get_project,
|
|
@@ -32,7 +50,7 @@ class TestStopCommand:
|
|
|
32
50
|
assert result.exit_code == 0
|
|
33
51
|
assert "Stopping" in result.output
|
|
34
52
|
assert "alice" in result.output
|
|
35
|
-
mock_container_manager.stop_container.assert_called_once_with("alice")
|
|
53
|
+
mock_container_manager.stop_container.assert_called_once_with("alice", remove=False)
|
|
36
54
|
|
|
37
55
|
@patch('devs.cli.get_project')
|
|
38
56
|
@patch('devs.cli.ContainerManager')
|
|
@@ -77,7 +95,7 @@ class TestStopCommand:
|
|
|
77
95
|
|
|
78
96
|
# Verify it completes
|
|
79
97
|
assert result.exit_code == 0
|
|
80
|
-
mock_container_manager.stop_container.assert_called_once_with("alice")
|
|
98
|
+
mock_container_manager.stop_container.assert_called_once_with("alice", remove=False)
|
|
81
99
|
|
|
82
100
|
@patch('devs.cli.get_project')
|
|
83
101
|
@patch('devs.cli.ContainerManager')
|
|
@@ -112,15 +130,16 @@ class TestStopCommand:
|
|
|
112
130
|
mock_get_project.return_value = mock_project
|
|
113
131
|
|
|
114
132
|
mock_container_manager = Mock()
|
|
115
|
-
mock_container_manager.stop_container.side_effect = ContainerError("Failed to stop")
|
|
133
|
+
mock_container_manager.stop_container.side_effect = [ContainerError("Failed to stop"), True]
|
|
116
134
|
mock_container_manager_class.return_value = mock_container_manager
|
|
117
135
|
|
|
118
|
-
|
|
119
|
-
result = cli_runner.invoke(cli, ['stop', 'alice'])
|
|
136
|
+
result = cli_runner.invoke(cli, ['stop', 'alice', 'bob'])
|
|
120
137
|
|
|
121
|
-
# The
|
|
122
|
-
|
|
123
|
-
|
|
138
|
+
# The error is reported, bob is still stopped, and the command exits non-zero
|
|
139
|
+
assert result.exit_code == 1
|
|
140
|
+
assert "Error stopping alice: Failed to stop" in result.output
|
|
141
|
+
assert mock_container_manager.stop_container.call_count == 2
|
|
142
|
+
mock_container_manager.stop_container.assert_called_with("bob", remove=False)
|
|
124
143
|
|
|
125
144
|
@patch('devs.cli.get_project')
|
|
126
145
|
@patch('devs.cli.ContainerManager')
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
"""Integration tests for the 'vscode' command."""
|
|
2
|
-
import json
|
|
3
2
|
from pathlib import Path
|
|
4
3
|
from unittest.mock import MagicMock, Mock, patch
|
|
5
4
|
|
|
@@ -12,7 +11,14 @@ from devs.exceptions import VSCodeError, WorkspaceError
|
|
|
12
11
|
|
|
13
12
|
|
|
14
13
|
class TestVSCodeCommand:
|
|
15
|
-
"""Test suite for 'devs vscode' command."""
|
|
14
|
+
"""Test suite for 'devs vscode' command in local mode (no SSH host)."""
|
|
15
|
+
|
|
16
|
+
@pytest.fixture(autouse=True)
|
|
17
|
+
def no_ssh_host(self, monkeypatch):
|
|
18
|
+
"""Keep these tests on the local path: no --ssh from env or DEVS.yml."""
|
|
19
|
+
monkeypatch.delenv('DEVS_SSH_HOST', raising=False)
|
|
20
|
+
with patch('devs.cli.DevsConfigLoader.load_ssh_host', return_value=None):
|
|
21
|
+
yield
|
|
16
22
|
|
|
17
23
|
@patch('devs.cli.get_project')
|
|
18
24
|
@patch('devs.cli.ContainerManager')
|
|
@@ -36,6 +42,7 @@ class TestVSCodeCommand:
|
|
|
36
42
|
mock_container_manager_class.return_value = mock_container_manager
|
|
37
43
|
|
|
38
44
|
mock_vscode = Mock()
|
|
45
|
+
mock_vscode.resolve_tailnet_ssh_host.return_value = None # not on the tailnet
|
|
39
46
|
mock_vscode.launch_multiple_devcontainers.return_value = 1
|
|
40
47
|
mock_vscode_class.return_value = mock_vscode
|
|
41
48
|
|
|
@@ -68,6 +75,7 @@ class TestVSCodeCommand:
|
|
|
68
75
|
mock_container_manager_class.return_value = mock_container_manager
|
|
69
76
|
|
|
70
77
|
mock_vscode = Mock()
|
|
78
|
+
mock_vscode.resolve_tailnet_ssh_host.return_value = None # not on the tailnet
|
|
71
79
|
mock_vscode.launch_multiple_devcontainers.return_value = 1
|
|
72
80
|
mock_vscode_class.return_value = mock_vscode
|
|
73
81
|
|
|
@@ -100,6 +108,7 @@ class TestVSCodeCommand:
|
|
|
100
108
|
mock_container_manager_class.return_value = mock_container_manager
|
|
101
109
|
|
|
102
110
|
mock_vscode = Mock()
|
|
111
|
+
mock_vscode.resolve_tailnet_ssh_host.return_value = None # not on the tailnet
|
|
103
112
|
mock_vscode_class.return_value = mock_vscode
|
|
104
113
|
|
|
105
114
|
# Run command
|
|
@@ -126,7 +135,7 @@ class TestVSCodeCommand:
|
|
|
126
135
|
mock_workspace_manager_class.return_value = mock_workspace_manager
|
|
127
136
|
|
|
128
137
|
mock_container_manager_class.return_value = Mock()
|
|
129
|
-
mock_vscode_class.return_value = Mock()
|
|
138
|
+
mock_vscode_class.return_value = Mock(**{'resolve_tailnet_ssh_host.return_value': None})
|
|
130
139
|
|
|
131
140
|
# Run command
|
|
132
141
|
result = cli_runner.invoke(cli, ['vscode', 'alice'])
|
|
@@ -156,6 +165,7 @@ class TestVSCodeCommand:
|
|
|
156
165
|
mock_container_manager_class.return_value = mock_container_manager
|
|
157
166
|
|
|
158
167
|
mock_vscode = Mock()
|
|
168
|
+
mock_vscode.resolve_tailnet_ssh_host.return_value = None # not on the tailnet
|
|
159
169
|
mock_vscode.launch_multiple_devcontainers.return_value = 1
|
|
160
170
|
mock_vscode_class.return_value = mock_vscode
|
|
161
171
|
|
|
@@ -187,6 +197,7 @@ class TestVSCodeCommand:
|
|
|
187
197
|
mock_container_manager_class.return_value = mock_container_manager
|
|
188
198
|
|
|
189
199
|
mock_vscode = Mock()
|
|
200
|
+
mock_vscode.resolve_tailnet_ssh_host.return_value = None # not on the tailnet
|
|
190
201
|
mock_vscode.launch_multiple_devcontainers.side_effect = VSCodeError("Failed to open")
|
|
191
202
|
mock_vscode_class.return_value = mock_vscode
|
|
192
203
|
|
|
@@ -220,6 +231,7 @@ class TestVSCodeCommand:
|
|
|
220
231
|
mock_container_manager_class.return_value = mock_container_manager
|
|
221
232
|
|
|
222
233
|
mock_vscode = Mock()
|
|
234
|
+
mock_vscode.resolve_tailnet_ssh_host.return_value = None # not on the tailnet
|
|
223
235
|
mock_vscode.launch_multiple_devcontainers.return_value = 1
|
|
224
236
|
mock_vscode_class.return_value = mock_vscode
|
|
225
237
|
|
|
@@ -251,6 +263,7 @@ class TestVSCodeCommand:
|
|
|
251
263
|
mock_container_manager_class.return_value = mock_container_manager
|
|
252
264
|
|
|
253
265
|
mock_vscode = Mock()
|
|
266
|
+
mock_vscode.resolve_tailnet_ssh_host.return_value = None # not on the tailnet
|
|
254
267
|
mock_vscode.launch_multiple_devcontainers.return_value = 1
|
|
255
268
|
mock_vscode_class.return_value = mock_vscode
|
|
256
269
|
|
|
@@ -351,7 +364,8 @@ class TestVSCodeSSHMode:
|
|
|
351
364
|
mock_vscode.launch_multiple_devcontainers.return_value = 1
|
|
352
365
|
mock_vscode_class.return_value = mock_vscode
|
|
353
366
|
|
|
354
|
-
|
|
367
|
+
# DEVS_SSH_HOST in the caller's environment would take priority over DEVS.yml
|
|
368
|
+
result = cli_runner.invoke(cli, ['vscode', 'alice'], env={'DEVS_SSH_HOST': None})
|
|
355
369
|
|
|
356
370
|
assert result.exit_code == 0
|
|
357
371
|
mock_container_manager_class.assert_not_called()
|
|
@@ -382,15 +396,12 @@ class TestSSHURIGeneration:
|
|
|
382
396
|
assert decoded == 'dev-test-org-repo-alice'
|
|
383
397
|
assert '/workspaces/test-org-repo-alice' in uri
|
|
384
398
|
|
|
385
|
-
def
|
|
386
|
-
"""SSH URI
|
|
399
|
+
def test_ssh_uri_is_direct_remote_ssh(self):
|
|
400
|
+
"""SSH URI opens the container's workspace directly over Remote-SSH (no attach)."""
|
|
387
401
|
vsi = self._make_vsi()
|
|
388
402
|
workspace_dir = Path('/home/user/.devs/workspaces/test-org-repo-alice')
|
|
389
403
|
uri = vsi.generate_devcontainer_uri(workspace_dir, 'alice', ssh_host='myhost.ts.net')
|
|
390
|
-
|
|
391
|
-
decoded = json.loads(bytes.fromhex(hex_part).decode('utf-8'))
|
|
392
|
-
assert decoded['containerName'] == '/dev-test-org-repo-alice'
|
|
393
|
-
assert decoded['settings']['host'] == 'ssh://myhost.ts.net'
|
|
404
|
+
assert uri == 'vscode-remote://ssh-remote+myhost.ts.net/workspaces/test-org-repo-alice'
|
|
394
405
|
|
|
395
406
|
def test_ssh_uri_workspace_path_correct(self):
|
|
396
407
|
"""SSH URI workspace path matches the devs naming convention."""
|
|
@@ -404,6 +415,4 @@ class TestSSHURIGeneration:
|
|
|
404
415
|
vsi = self._make_vsi()
|
|
405
416
|
workspace_dir = Path('/home/user/.devs/workspaces/test-org-repo-alice')
|
|
406
417
|
uri = vsi.generate_devcontainer_uri(workspace_dir, 'alice', ssh_host='myhost.ts.net:2222')
|
|
407
|
-
|
|
408
|
-
decoded = json.loads(bytes.fromhex(hex_part).decode('utf-8'))
|
|
409
|
-
assert decoded['settings']['host'] == 'ssh://myhost.ts.net:2222'
|
|
418
|
+
assert uri.startswith('vscode-remote://ssh-remote+myhost.ts.net:2222/')
|
|
@@ -96,12 +96,14 @@ class TestLiveMode:
|
|
|
96
96
|
# Mock VSCodeIntegration
|
|
97
97
|
with patch('devs.cli.VSCodeIntegration') as mock_vscode:
|
|
98
98
|
mock_vscode_instance = Mock()
|
|
99
|
+
mock_vscode_instance.resolve_tailnet_ssh_host.return_value = None # not on the tailnet
|
|
99
100
|
mock_vscode_instance.launch_multiple_devcontainers.return_value = 1
|
|
100
101
|
mock_vscode.return_value = mock_vscode_instance
|
|
101
102
|
|
|
102
103
|
# Run command
|
|
103
104
|
runner = CliRunner()
|
|
104
|
-
|
|
105
|
+
with patch('devs.cli.DevsConfigLoader.load_ssh_host', return_value=None):
|
|
106
|
+
result = runner.invoke(cli, ['vscode', 'test-dev', '--live'], env={'DEVS_SSH_HOST': None})
|
|
105
107
|
|
|
106
108
|
# Verify live mode was passed
|
|
107
109
|
mock_container.ensure_container_running.assert_called_once()
|
|
@@ -135,12 +137,7 @@ class TestLiveMode:
|
|
|
135
137
|
runner = CliRunner()
|
|
136
138
|
result = runner.invoke(cli, ['shell', 'test-dev', '--live'])
|
|
137
139
|
|
|
138
|
-
# Verify live
|
|
139
|
-
mock_container.ensure_container_running.assert_called_once()
|
|
140
|
-
call_args = mock_container.ensure_container_running.call_args
|
|
141
|
-
assert call_args.kwargs.get('live') is True
|
|
142
|
-
|
|
143
|
-
# Verify exec_shell was called with live=True
|
|
140
|
+
# Verify exec_shell was called with live=True (it starts the container internally)
|
|
144
141
|
mock_container.exec_shell.assert_called_once()
|
|
145
142
|
call_args = mock_container.exec_shell.call_args
|
|
146
143
|
assert call_args.kwargs.get('live') is True
|
|
@@ -161,7 +158,7 @@ class TestLiveMode:
|
|
|
161
158
|
|
|
162
159
|
mock_container = Mock()
|
|
163
160
|
mock_container.ensure_container_running.return_value = True
|
|
164
|
-
mock_container.
|
|
161
|
+
mock_container.exec_agent = Mock(return_value=(True, "output", ""))
|
|
165
162
|
mock_container_mgr.return_value = mock_container
|
|
166
163
|
|
|
167
164
|
mock_workspace = Mock()
|
|
@@ -172,14 +169,9 @@ class TestLiveMode:
|
|
|
172
169
|
runner = CliRunner()
|
|
173
170
|
result = runner.invoke(cli, ['claude', 'test-dev', 'test prompt', '--live'])
|
|
174
171
|
|
|
175
|
-
# Verify live
|
|
176
|
-
mock_container.
|
|
177
|
-
call_args = mock_container.
|
|
178
|
-
assert call_args.kwargs.get('live') is True
|
|
179
|
-
|
|
180
|
-
# Verify exec_claude was called with live=True
|
|
181
|
-
mock_container.exec_claude.assert_called_once()
|
|
182
|
-
call_args = mock_container.exec_claude.call_args
|
|
172
|
+
# Verify exec_agent was called with live=True (it starts the container internally)
|
|
173
|
+
mock_container.exec_agent.assert_called_once()
|
|
174
|
+
call_args = mock_container.exec_agent.call_args
|
|
183
175
|
assert call_args.kwargs.get('live') is True
|
|
184
176
|
|
|
185
177
|
# Verify workspace.create_workspace was called with live=True and reset_contents=False
|
|
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
|