devs-cli 0.1.4__tar.gz → 0.1.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 (30) hide show
  1. {devs_cli-0.1.4/devs_cli.egg-info → devs_cli-0.1.5}/PKG-INFO +1 -1
  2. {devs_cli-0.1.4 → devs_cli-0.1.5}/devs/cli.py +182 -0
  3. {devs_cli-0.1.4 → devs_cli-0.1.5}/devs/config.py +10 -2
  4. {devs_cli-0.1.4 → devs_cli-0.1.5/devs_cli.egg-info}/PKG-INFO +1 -1
  5. {devs_cli-0.1.4 → devs_cli-0.1.5}/pyproject.toml +1 -1
  6. {devs_cli-0.1.4 → devs_cli-0.1.5}/tests/test_cli.py +82 -0
  7. {devs_cli-0.1.4 → devs_cli-0.1.5}/LICENSE +0 -0
  8. {devs_cli-0.1.4 → devs_cli-0.1.5}/README.md +0 -0
  9. {devs_cli-0.1.4 → devs_cli-0.1.5}/devs/__init__.py +0 -0
  10. {devs_cli-0.1.4 → devs_cli-0.1.5}/devs/core/__init__.py +0 -0
  11. {devs_cli-0.1.4 → devs_cli-0.1.5}/devs/core/integration.py +0 -0
  12. {devs_cli-0.1.4 → devs_cli-0.1.5}/devs/exceptions.py +0 -0
  13. {devs_cli-0.1.4 → devs_cli-0.1.5}/devs/utils/__init__.py +0 -0
  14. {devs_cli-0.1.4 → devs_cli-0.1.5}/devs_cli.egg-info/SOURCES.txt +0 -0
  15. {devs_cli-0.1.4 → devs_cli-0.1.5}/devs_cli.egg-info/dependency_links.txt +0 -0
  16. {devs_cli-0.1.4 → devs_cli-0.1.5}/devs_cli.egg-info/entry_points.txt +0 -0
  17. {devs_cli-0.1.4 → devs_cli-0.1.5}/devs_cli.egg-info/requires.txt +0 -0
  18. {devs_cli-0.1.4 → devs_cli-0.1.5}/devs_cli.egg-info/top_level.txt +0 -0
  19. {devs_cli-0.1.4 → devs_cli-0.1.5}/setup.cfg +0 -0
  20. {devs_cli-0.1.4 → devs_cli-0.1.5}/tests/test_cli_clean.py +0 -0
  21. {devs_cli-0.1.4 → devs_cli-0.1.5}/tests/test_cli_misc.py +0 -0
  22. {devs_cli-0.1.4 → devs_cli-0.1.5}/tests/test_cli_start.py +0 -0
  23. {devs_cli-0.1.4 → devs_cli-0.1.5}/tests/test_cli_stop.py +0 -0
  24. {devs_cli-0.1.4 → devs_cli-0.1.5}/tests/test_cli_vscode.py +0 -0
  25. {devs_cli-0.1.4 → devs_cli-0.1.5}/tests/test_container_manager.py +0 -0
  26. {devs_cli-0.1.4 → devs_cli-0.1.5}/tests/test_e2e.py +0 -0
  27. {devs_cli-0.1.4 → devs_cli-0.1.5}/tests/test_integration.py +0 -0
  28. {devs_cli-0.1.4 → devs_cli-0.1.5}/tests/test_live_mode.py +0 -0
  29. {devs_cli-0.1.4 → devs_cli-0.1.5}/tests/test_project.py +0 -0
  30. {devs_cli-0.1.4 → devs_cli-0.1.5}/tests/test_workspace_manager.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devs-cli
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Summary: DevContainer Management Tool - Manage multiple named devcontainers for any project
5
5
  Author: Dan Lester
6
6
  License-Expression: MIT
@@ -509,6 +509,188 @@ def _handle_claude_auth(api_key: str, debug: bool) -> None:
509
509
  sys.exit(1)
510
510
 
511
511
 
512
+ @cli.command()
513
+ @click.argument('dev_name', required=False)
514
+ @click.argument('prompt', required=False)
515
+ @click.option('--auth', is_flag=True, help='Set up Codex authentication for devcontainers')
516
+ @click.option('--api-key', help='OpenAI API key to authenticate with (use with --auth)')
517
+ @click.option('--reset-workspace', is_flag=True, help='Reset workspace contents before execution')
518
+ @click.option('--live', is_flag=True, help='Start container with current directory mounted as workspace')
519
+ @click.option('--env', multiple=True, help='Environment variables to pass to container (format: VAR=value)')
520
+ @debug_option
521
+ def codex(dev_name: str, prompt: str, auth: bool, api_key: str, reset_workspace: bool, live: bool, env: tuple, debug: bool) -> None:
522
+ """Execute OpenAI Codex CLI in devcontainer or set up authentication.
523
+
524
+ DEV_NAME: Development environment name
525
+ PROMPT: Prompt to send to Codex
526
+
527
+ Example: devs codex sally "Summarize this codebase"
528
+ Example: devs codex sally "Fix the tests" --reset-workspace
529
+ Example: devs codex sally "Fix the tests" --live # Run with current directory
530
+ Example: devs codex sally "Start the server" --env QUART_PORT=5001
531
+ Example: devs codex --auth # Interactive authentication
532
+ Example: devs codex --auth --api-key <YOUR_KEY> # API key authentication
533
+ """
534
+ # Handle authentication mode
535
+ if auth:
536
+ _handle_codex_auth(api_key=api_key, debug=debug)
537
+ return
538
+
539
+ # Validate required arguments for execution mode
540
+ if not dev_name or not prompt:
541
+ raise click.UsageError("DEV_NAME and PROMPT are required unless using --auth")
542
+
543
+ check_dependencies()
544
+ project = get_project()
545
+
546
+ # Load environment variables from DEVS.yml and merge with CLI --env flags
547
+ devs_env = DevsConfigLoader.load_env_vars(dev_name, project.info.name)
548
+ cli_env = parse_env_vars(env) if env else {}
549
+ extra_env = merge_env_vars(devs_env, cli_env) if devs_env or cli_env else None
550
+
551
+ if extra_env:
552
+ console.print(f"🔧 Environment variables: {', '.join(f'{k}={v}' for k, v in extra_env.items())}")
553
+
554
+ container_manager = ContainerManager(project, config)
555
+ workspace_manager = WorkspaceManager(project, config)
556
+
557
+ try:
558
+ # Ensure workspace exists (handles live mode and reset internally)
559
+ workspace_dir = workspace_manager.create_workspace(dev_name, reset_contents=reset_workspace, live=live)
560
+ # Ensure container is running
561
+ container_manager.ensure_container_running(
562
+ dev_name=dev_name,
563
+ workspace_dir=workspace_dir,
564
+ force_rebuild=False,
565
+ debug=debug,
566
+ live=live,
567
+ extra_env=extra_env
568
+ )
569
+
570
+ # Execute Codex
571
+ console.print(f"🤖 Executing Codex in {dev_name}...")
572
+ if reset_workspace and not live:
573
+ console.print("🗑️ Workspace contents reset")
574
+ console.print(f"📝 Prompt: {prompt}")
575
+ console.print("")
576
+
577
+ success, output, error = container_manager.exec_codex(
578
+ dev_name=dev_name,
579
+ workspace_dir=workspace_dir,
580
+ prompt=prompt,
581
+ debug=debug,
582
+ stream=True,
583
+ live=live,
584
+ extra_env=extra_env
585
+ )
586
+
587
+ console.print("") # Add spacing after streamed output
588
+ if success:
589
+ console.print("✅ Codex execution completed")
590
+ else:
591
+ console.print("❌ Codex execution failed")
592
+ if error:
593
+ console.print("")
594
+ console.print("🚫 Error:")
595
+ console.print(error)
596
+ sys.exit(1)
597
+
598
+ except (ContainerError, WorkspaceError) as e:
599
+ console.print(f"❌ Error executing Codex in {dev_name}: {e}")
600
+ sys.exit(1)
601
+
602
+
603
+ def _handle_codex_auth(api_key: str, debug: bool) -> None:
604
+ """Handle Codex authentication setup.
605
+
606
+ This configures Codex authentication that will be shared across
607
+ all devcontainers for this project. The authentication is stored
608
+ on the host and bind-mounted into containers.
609
+ """
610
+ try:
611
+ # Ensure Codex config directory exists
612
+ config.ensure_directories()
613
+
614
+ console.print("🔐 Setting up Codex authentication...")
615
+ console.print(f" Configuration will be saved to: {config.codex_config_dir}")
616
+
617
+ if api_key:
618
+ # Set API key directly using Codex CLI
619
+ console.print(" Using provided API key...")
620
+
621
+ # Set CODEX_CONFIG_HOME to our config directory and run auth with API key
622
+ env = os.environ.copy()
623
+ env['CODEX_CONFIG_HOME'] = str(config.codex_config_dir)
624
+
625
+ cmd = ['codex', 'auth', '--api-key', api_key]
626
+
627
+ if debug:
628
+ console.print(f"[dim]Running: {' '.join(cmd)}[/dim]")
629
+ console.print(f"[dim]CODEX_CONFIG_HOME: {config.codex_config_dir}[/dim]")
630
+
631
+ result = subprocess.run(
632
+ cmd,
633
+ env=env,
634
+ capture_output=True,
635
+ text=True
636
+ )
637
+
638
+ if result.returncode != 0:
639
+ error_msg = result.stderr or result.stdout or "Unknown error"
640
+ raise Exception(f"Codex authentication failed: {error_msg}")
641
+
642
+ else:
643
+ # Interactive authentication
644
+ console.print(" Starting interactive authentication...")
645
+ console.print(" Follow the prompts to authenticate with Codex")
646
+ console.print("")
647
+
648
+ # Set CODEX_CONFIG_HOME to our config directory
649
+ env = os.environ.copy()
650
+ env['CODEX_CONFIG_HOME'] = str(config.codex_config_dir)
651
+
652
+ cmd = ['codex', 'auth']
653
+
654
+ if debug:
655
+ console.print(f"[dim]Running: {' '.join(cmd)}[/dim]")
656
+ console.print(f"[dim]CODEX_CONFIG_HOME: {config.codex_config_dir}[/dim]")
657
+
658
+ # Run interactively
659
+ result = subprocess.run(
660
+ cmd,
661
+ env=env,
662
+ check=False
663
+ )
664
+
665
+ if result.returncode != 0:
666
+ raise Exception("Codex authentication was cancelled or failed")
667
+
668
+ console.print("")
669
+ console.print("✅ Codex authentication configured successfully!")
670
+ console.print(f" Configuration saved to: {config.codex_config_dir}")
671
+ console.print(" This authentication will be shared across all devcontainers")
672
+ console.print("")
673
+ console.print("💡 You can now use Codex in any devcontainer:")
674
+ console.print(" devs codex <dev-name> 'Your prompt here'")
675
+
676
+ except FileNotFoundError:
677
+ console.print("❌ Codex CLI not found on host machine")
678
+ console.print("")
679
+ console.print("Please install Codex CLI first:")
680
+ console.print(" npm install -g @openai/codex")
681
+ console.print("")
682
+ console.print("Note: Codex needs to be installed on the host machine")
683
+ console.print(" for authentication. It's already available in containers.")
684
+ sys.exit(1)
685
+
686
+ except Exception as e:
687
+ console.print(f"❌ Failed to configure Codex authentication: {e}")
688
+ if debug:
689
+ import traceback
690
+ console.print(traceback.format_exc())
691
+ sys.exit(1)
692
+
693
+
512
694
  @cli.command()
513
695
  @click.argument('dev_name')
514
696
  @click.option('--reset-workspace', is_flag=True, help='Reset workspace contents before execution')
@@ -15,17 +15,24 @@ class Config(BaseConfig):
15
15
  WORKSPACES_DIR = Path.home() / ".devs" / "workspaces"
16
16
  BRIDGE_DIR = Path.home() / ".devs" / "bridge"
17
17
  CLAUDE_CONFIG_DIR = Path.home() / ".devs" / "claudeconfig"
18
-
18
+ CODEX_CONFIG_DIR = Path.home() / ".devs" / "codexconfig"
19
+
19
20
  def __init__(self) -> None:
20
21
  """Initialize configuration with environment variable overrides."""
21
22
  super().__init__()
22
-
23
+
23
24
  # CLI-specific configuration
24
25
  claude_config_env = os.getenv("DEVS_CLAUDE_CONFIG_DIR")
25
26
  if claude_config_env:
26
27
  self.claude_config_dir = Path(claude_config_env)
27
28
  else:
28
29
  self.claude_config_dir = self.CLAUDE_CONFIG_DIR
30
+
31
+ codex_config_env = os.getenv("DEVS_CODEX_CONFIG_DIR")
32
+ if codex_config_env:
33
+ self.codex_config_dir = Path(codex_config_env)
34
+ else:
35
+ self.codex_config_dir = self.CODEX_CONFIG_DIR
29
36
 
30
37
  def get_default_workspaces_dir(self) -> Path:
31
38
  """Get default workspaces directory for CLI package."""
@@ -43,6 +50,7 @@ class Config(BaseConfig):
43
50
  """Ensure required directories exist."""
44
51
  super().ensure_directories()
45
52
  self.claude_config_dir.mkdir(parents=True, exist_ok=True)
53
+ self.codex_config_dir.mkdir(parents=True, exist_ok=True)
46
54
 
47
55
 
48
56
  # Global config instance
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devs-cli
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Summary: DevContainer Management Tool - Manage multiple named devcontainers for any project
5
5
  Author: Dan Lester
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devs-cli"
7
- version = "0.1.4"
7
+ version = "0.1.5"
8
8
  description = "DevContainer Management Tool - Manage multiple named devcontainers for any project"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -202,5 +202,87 @@ class TestCLI:
202
202
  runner = CliRunner()
203
203
  result = runner.invoke(cli, ['claude'])
204
204
 
205
+ assert result.exit_code != 0
206
+ assert "DEV_NAME and PROMPT are required unless using --auth" in result.output
207
+
208
+ def test_codex_command_help(self):
209
+ """Test codex command help."""
210
+ runner = CliRunner()
211
+ result = runner.invoke(cli, ['codex', '--help'])
212
+
213
+ assert result.exit_code == 0
214
+ assert "Execute OpenAI Codex CLI in devcontainer or set up authentication" in result.output
215
+ assert "--auth" in result.output
216
+ assert "--api-key" in result.output
217
+
218
+ @patch('devs.cli.subprocess.run')
219
+ @patch('devs.cli.config')
220
+ def test_codex_auth_with_api_key(self, mock_config, mock_subprocess):
221
+ """Test codex --auth command with API key."""
222
+ # Setup mocks
223
+ mock_config.codex_config_dir = '/tmp/test-codex-config'
224
+ mock_config.ensure_directories = Mock()
225
+ mock_subprocess.return_value.returncode = 0
226
+
227
+ runner = CliRunner()
228
+ result = runner.invoke(cli, ['codex', '--auth', '--api-key', 'test-key-123'])
229
+
230
+ assert result.exit_code == 0
231
+ assert "Setting up Codex authentication" in result.output
232
+ assert "Codex authentication configured successfully" in result.output
233
+
234
+ # Verify subprocess was called with correct arguments
235
+ mock_subprocess.assert_called_once()
236
+ call_args = mock_subprocess.call_args
237
+ assert 'codex' in call_args[0][0]
238
+ assert 'auth' in call_args[0][0]
239
+ assert '--api-key' in call_args[0][0]
240
+ assert 'test-key-123' in call_args[0][0]
241
+
242
+ @patch('devs.cli.subprocess.run')
243
+ @patch('devs.cli.config')
244
+ def test_codex_auth_interactive(self, mock_config, mock_subprocess):
245
+ """Test codex --auth command in interactive mode."""
246
+ # Setup mocks
247
+ mock_config.codex_config_dir = '/tmp/test-codex-config'
248
+ mock_config.ensure_directories = Mock()
249
+ mock_subprocess.return_value.returncode = 0
250
+
251
+ runner = CliRunner()
252
+ result = runner.invoke(cli, ['codex', '--auth'])
253
+
254
+ assert result.exit_code == 0
255
+ assert "Setting up Codex authentication" in result.output
256
+ assert "Starting interactive authentication" in result.output
257
+ assert "Codex authentication configured successfully" in result.output
258
+
259
+ # Verify subprocess was called for interactive auth
260
+ mock_subprocess.assert_called_once()
261
+ call_args = mock_subprocess.call_args
262
+ assert 'codex' in call_args[0][0]
263
+ assert 'auth' in call_args[0][0]
264
+ assert '--api-key' not in call_args[0][0]
265
+
266
+ @patch('devs.cli.subprocess.run')
267
+ @patch('devs.cli.config')
268
+ def test_codex_auth_command_not_found(self, mock_config, mock_subprocess):
269
+ """Test codex --auth when codex CLI is not installed."""
270
+ # Setup mocks
271
+ mock_config.codex_config_dir = '/tmp/test-codex-config'
272
+ mock_config.ensure_directories = Mock()
273
+ mock_subprocess.side_effect = FileNotFoundError()
274
+
275
+ runner = CliRunner()
276
+ result = runner.invoke(cli, ['codex', '--auth'])
277
+
278
+ assert result.exit_code == 1
279
+ assert "Codex CLI not found" in result.output
280
+ assert "npm install -g @openai/codex" in result.output
281
+
282
+ def test_codex_missing_args(self):
283
+ """Test codex command without required args (not using --auth)."""
284
+ runner = CliRunner()
285
+ result = runner.invoke(cli, ['codex'])
286
+
205
287
  assert result.exit_code != 0
206
288
  assert "DEV_NAME and PROMPT are required unless using --auth" in result.output
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes