devs-cli 5.0.5__py3-none-any.whl → 6.0.0__py3-none-any.whl

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.py CHANGED
@@ -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
- container_manager.stop_container(dev_name, remove=False)
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
- @cli.command()
428
- @click.argument('dev_name', required=False)
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 Claude (ensure_container_running is called internally)
486
- console.print(f"🤖 Executing Claude in {dev_name}...")
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.exec_claude(
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("✅ Claude execution completed")
481
+ console.print(f"✅ {agent.display_name} execution completed")
505
482
  else:
506
- console.print("❌ Claude execution failed")
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 Claude in {dev_name}: {e}")
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
- except Exception as e:
520
- console.print(f"❌ Failed to configure Claude authentication: {e}")
521
- if debug:
522
- console.print(traceback.format_exc())
523
- sys.exit(1)
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
- # Handle authentication mode
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
- try:
572
- # Ensure workspace exists (handles live mode and reset internally)
573
- workspace_dir = workspace_manager.create_workspace(dev_name, reset_contents=reset_workspace, live=live)
574
-
575
- # Execute Codex (ensure_container_running is called internally)
576
- console.print(f"🤖 Executing Codex in {dev_name}...")
577
- if reset_workspace and not live:
578
- console.print("🗑️ Workspace contents reset")
579
- console.print(f"📝 Prompt: {prompt}")
580
- console.print("")
581
-
582
- success, output, error, _ = container_manager.exec_codex(
583
- dev_name=dev_name,
584
- workspace_dir=workspace_dir,
585
- prompt=prompt,
586
- debug=debug,
587
- stream=True,
588
- live=live,
589
- extra_env=extra_env
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
- except (ContainerError, WorkspaceError) as e:
604
- console.print(f"❌ Error executing Codex in {dev_name}: {e}")
605
- sys.exit(1)
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
- # Set CODEX_CONFIG_HOME to our config directory and run auth with API key
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
- # Set CODEX_CONFIG_HOME to our config directory
654
- env = os.environ.copy()
655
- env['CODEX_CONFIG_HOME'] = str(config.codex_config_dir)
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')
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devs-cli
3
- Version: 5.0.5
3
+ Version: 6.0.0
4
4
  Summary: DevContainer Management Tool - Manage multiple named devcontainers for any project
5
5
  Author: Dan Lester
6
6
  License-Expression: MIT
@@ -1,13 +1,13 @@
1
1
  devs/__init__.py,sha256=dMk0J_JrmBmknPvt6HNUa04qxgHYpxhnIXa3PZFk_N0,571
2
- devs/cli.py,sha256=inWDnumsDeaQcvY8y42HcNPY-HJxYkCOUGHgsjYx4nI,44985
2
+ devs/cli.py,sha256=zBnWCQeby2aCAIW96ilnI246gbN1DQlps7J9a14vJ7g,44360
3
3
  devs/config.py,sha256=DaS2_1x0h63cHn34RmWbEhUx_1u3-sAo16tM3AXMKfQ,1561
4
4
  devs/exceptions.py,sha256=7xO7ihJu_U6CupZPMv89B4N5EBSUcdj2OdA6GPAFPEE,490
5
5
  devs/core/__init__.py,sha256=TPy3eZi-AEztci_QZ37YAAl2zvUQlwBALpyiQx2ED7Q,363
6
6
  devs/core/integration.py,sha256=_zT5kJi_L34uU3yhLinLI7bbjI5Q27dX9ABxgwHkx2Y,16557
7
7
  devs/utils/__init__.py,sha256=f-sEWETPfW2Xkaxgd-l6FS-jIQAorLlQZOicIZvp-W0,638
8
- devs_cli-5.0.5.dist-info/licenses/LICENSE,sha256=bi2EUiv-lmC_quQVkNqzTYXJpjVarkPsVKfqhJl7ccQ,1067
9
- devs_cli-5.0.5.dist-info/METADATA,sha256=OufauzLvinQNxXkMprQG2ex9aWEGEdnzrDnSc7zkKx8,5282
10
- devs_cli-5.0.5.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
11
- devs_cli-5.0.5.dist-info/entry_points.txt,sha256=s8-pgqZ1QvzPJgHP9vNxwBYdezmGsFdRUHacJPZ4WOs,39
12
- devs_cli-5.0.5.dist-info/top_level.txt,sha256=9RIVUPVGuOdO84qkBh_9XcKoTV7773o3py78wz2-IZk,5
13
- devs_cli-5.0.5.dist-info/RECORD,,
8
+ devs_cli-6.0.0.dist-info/licenses/LICENSE,sha256=bi2EUiv-lmC_quQVkNqzTYXJpjVarkPsVKfqhJl7ccQ,1067
9
+ devs_cli-6.0.0.dist-info/METADATA,sha256=Jy_OaQ3IuSWwwguszCd1OcWQOi5KLmeGrMWrsAJAS58,5282
10
+ devs_cli-6.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
11
+ devs_cli-6.0.0.dist-info/entry_points.txt,sha256=s8-pgqZ1QvzPJgHP9vNxwBYdezmGsFdRUHacJPZ4WOs,39
12
+ devs_cli-6.0.0.dist-info/top_level.txt,sha256=9RIVUPVGuOdO84qkBh_9XcKoTV7773o3py78wz2-IZk,5
13
+ devs_cli-6.0.0.dist-info/RECORD,,