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.
- {devs_cli-0.1.4/devs_cli.egg-info → devs_cli-0.1.5}/PKG-INFO +1 -1
- {devs_cli-0.1.4 → devs_cli-0.1.5}/devs/cli.py +182 -0
- {devs_cli-0.1.4 → devs_cli-0.1.5}/devs/config.py +10 -2
- {devs_cli-0.1.4 → devs_cli-0.1.5/devs_cli.egg-info}/PKG-INFO +1 -1
- {devs_cli-0.1.4 → devs_cli-0.1.5}/pyproject.toml +1 -1
- {devs_cli-0.1.4 → devs_cli-0.1.5}/tests/test_cli.py +82 -0
- {devs_cli-0.1.4 → devs_cli-0.1.5}/LICENSE +0 -0
- {devs_cli-0.1.4 → devs_cli-0.1.5}/README.md +0 -0
- {devs_cli-0.1.4 → devs_cli-0.1.5}/devs/__init__.py +0 -0
- {devs_cli-0.1.4 → devs_cli-0.1.5}/devs/core/__init__.py +0 -0
- {devs_cli-0.1.4 → devs_cli-0.1.5}/devs/core/integration.py +0 -0
- {devs_cli-0.1.4 → devs_cli-0.1.5}/devs/exceptions.py +0 -0
- {devs_cli-0.1.4 → devs_cli-0.1.5}/devs/utils/__init__.py +0 -0
- {devs_cli-0.1.4 → devs_cli-0.1.5}/devs_cli.egg-info/SOURCES.txt +0 -0
- {devs_cli-0.1.4 → devs_cli-0.1.5}/devs_cli.egg-info/dependency_links.txt +0 -0
- {devs_cli-0.1.4 → devs_cli-0.1.5}/devs_cli.egg-info/entry_points.txt +0 -0
- {devs_cli-0.1.4 → devs_cli-0.1.5}/devs_cli.egg-info/requires.txt +0 -0
- {devs_cli-0.1.4 → devs_cli-0.1.5}/devs_cli.egg-info/top_level.txt +0 -0
- {devs_cli-0.1.4 → devs_cli-0.1.5}/setup.cfg +0 -0
- {devs_cli-0.1.4 → devs_cli-0.1.5}/tests/test_cli_clean.py +0 -0
- {devs_cli-0.1.4 → devs_cli-0.1.5}/tests/test_cli_misc.py +0 -0
- {devs_cli-0.1.4 → devs_cli-0.1.5}/tests/test_cli_start.py +0 -0
- {devs_cli-0.1.4 → devs_cli-0.1.5}/tests/test_cli_stop.py +0 -0
- {devs_cli-0.1.4 → devs_cli-0.1.5}/tests/test_cli_vscode.py +0 -0
- {devs_cli-0.1.4 → devs_cli-0.1.5}/tests/test_container_manager.py +0 -0
- {devs_cli-0.1.4 → devs_cli-0.1.5}/tests/test_e2e.py +0 -0
- {devs_cli-0.1.4 → devs_cli-0.1.5}/tests/test_integration.py +0 -0
- {devs_cli-0.1.4 → devs_cli-0.1.5}/tests/test_live_mode.py +0 -0
- {devs_cli-0.1.4 → devs_cli-0.1.5}/tests/test_project.py +0 -0
- {devs_cli-0.1.4 → devs_cli-0.1.5}/tests/test_workspace_manager.py +0 -0
|
@@ -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
|
|
@@ -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
|
|
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
|