devs-cli 0.1.3__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.3/devs_cli.egg-info → devs_cli-0.1.5}/PKG-INFO +3 -3
  2. {devs_cli-0.1.3 → devs_cli-0.1.5}/README.md +2 -2
  3. {devs_cli-0.1.3 → devs_cli-0.1.5}/devs/cli.py +310 -122
  4. {devs_cli-0.1.3 → devs_cli-0.1.5}/devs/config.py +10 -2
  5. {devs_cli-0.1.3 → devs_cli-0.1.5/devs_cli.egg-info}/PKG-INFO +3 -3
  6. {devs_cli-0.1.3 → devs_cli-0.1.5}/pyproject.toml +2 -2
  7. {devs_cli-0.1.3 → devs_cli-0.1.5}/tests/test_cli.py +114 -23
  8. {devs_cli-0.1.3 → devs_cli-0.1.5}/LICENSE +0 -0
  9. {devs_cli-0.1.3 → devs_cli-0.1.5}/devs/__init__.py +0 -0
  10. {devs_cli-0.1.3 → devs_cli-0.1.5}/devs/core/__init__.py +0 -0
  11. {devs_cli-0.1.3 → devs_cli-0.1.5}/devs/core/integration.py +0 -0
  12. {devs_cli-0.1.3 → devs_cli-0.1.5}/devs/exceptions.py +0 -0
  13. {devs_cli-0.1.3 → devs_cli-0.1.5}/devs/utils/__init__.py +0 -0
  14. {devs_cli-0.1.3 → devs_cli-0.1.5}/devs_cli.egg-info/SOURCES.txt +0 -0
  15. {devs_cli-0.1.3 → devs_cli-0.1.5}/devs_cli.egg-info/dependency_links.txt +0 -0
  16. {devs_cli-0.1.3 → devs_cli-0.1.5}/devs_cli.egg-info/entry_points.txt +0 -0
  17. {devs_cli-0.1.3 → devs_cli-0.1.5}/devs_cli.egg-info/requires.txt +0 -0
  18. {devs_cli-0.1.3 → devs_cli-0.1.5}/devs_cli.egg-info/top_level.txt +0 -0
  19. {devs_cli-0.1.3 → devs_cli-0.1.5}/setup.cfg +0 -0
  20. {devs_cli-0.1.3 → devs_cli-0.1.5}/tests/test_cli_clean.py +0 -0
  21. {devs_cli-0.1.3 → devs_cli-0.1.5}/tests/test_cli_misc.py +0 -0
  22. {devs_cli-0.1.3 → devs_cli-0.1.5}/tests/test_cli_start.py +0 -0
  23. {devs_cli-0.1.3 → devs_cli-0.1.5}/tests/test_cli_stop.py +0 -0
  24. {devs_cli-0.1.3 → devs_cli-0.1.5}/tests/test_cli_vscode.py +0 -0
  25. {devs_cli-0.1.3 → devs_cli-0.1.5}/tests/test_container_manager.py +0 -0
  26. {devs_cli-0.1.3 → devs_cli-0.1.5}/tests/test_e2e.py +0 -0
  27. {devs_cli-0.1.3 → devs_cli-0.1.5}/tests/test_integration.py +0 -0
  28. {devs_cli-0.1.3 → devs_cli-0.1.5}/tests/test_live_mode.py +0 -0
  29. {devs_cli-0.1.3 → devs_cli-0.1.5}/tests/test_project.py +0 -0
  30. {devs_cli-0.1.3 → 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.3
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
@@ -81,9 +81,9 @@ devs start frontend --env DEBUG=true --env API_URL=http://localhost:3000
81
81
  devs claude frontend "Fix the tests" --env NODE_ENV=test
82
82
 
83
83
  # Set up Claude authentication (once per host)
84
- devs claude-auth
84
+ devs claude --auth
85
85
  # Or with API key
86
- devs claude-auth --api-key <YOUR_API_KEY>
86
+ devs claude --auth --api-key <YOUR_API_KEY>
87
87
 
88
88
  # Clean up when done
89
89
  devs stop frontend backend
@@ -40,9 +40,9 @@ devs start frontend --env DEBUG=true --env API_URL=http://localhost:3000
40
40
  devs claude frontend "Fix the tests" --env NODE_ENV=test
41
41
 
42
42
  # Set up Claude authentication (once per host)
43
- devs claude-auth
43
+ devs claude --auth
44
44
  # Or with API key
45
- devs claude-auth --api-key <YOUR_API_KEY>
45
+ devs claude --auth --api-key <YOUR_API_KEY>
46
46
 
47
47
  # Clean up when done
48
48
  devs stop frontend backend
@@ -328,67 +328,80 @@ def shell(dev_name: str, live: bool, env: tuple, debug: bool) -> None:
328
328
 
329
329
 
330
330
  @cli.command()
331
- @click.argument('dev_name')
332
- @click.argument('prompt')
331
+ @click.argument('dev_name', required=False)
332
+ @click.argument('prompt', required=False)
333
+ @click.option('--auth', is_flag=True, help='Set up Claude authentication for devcontainers')
334
+ @click.option('--api-key', help='Claude API key to authenticate with (use with --auth)')
333
335
  @click.option('--reset-workspace', is_flag=True, help='Reset workspace contents before execution')
334
336
  @click.option('--live', is_flag=True, help='Start container with current directory mounted as workspace')
335
337
  @click.option('--env', multiple=True, help='Environment variables to pass to container (format: VAR=value)')
336
338
  @debug_option
337
- def claude(dev_name: str, prompt: str, reset_workspace: bool, live: bool, env: tuple, debug: bool) -> None:
338
- """Execute Claude CLI in devcontainer.
339
-
339
+ def claude(dev_name: str, prompt: str, auth: bool, api_key: str, reset_workspace: bool, live: bool, env: tuple, debug: bool) -> None:
340
+ """Execute Claude CLI in devcontainer or set up authentication.
341
+
340
342
  DEV_NAME: Development environment name
341
343
  PROMPT: Prompt to send to Claude
342
-
344
+
343
345
  Example: devs claude sally "Summarize this codebase"
344
346
  Example: devs claude sally "Fix the tests" --reset-workspace
345
347
  Example: devs claude sally "Fix the tests" --live # Run with current directory
346
348
  Example: devs claude sally "Start the server" --env QUART_PORT=5001
349
+ Example: devs claude --auth # Interactive authentication
350
+ Example: devs claude --auth --api-key <YOUR_KEY> # API key authentication
347
351
  """
352
+ # Handle authentication mode
353
+ if auth:
354
+ _handle_claude_auth(api_key=api_key, debug=debug)
355
+ return
356
+
357
+ # Validate required arguments for execution mode
358
+ if not dev_name or not prompt:
359
+ raise click.UsageError("DEV_NAME and PROMPT are required unless using --auth")
360
+
348
361
  check_dependencies()
349
362
  project = get_project()
350
-
363
+
351
364
  # Load environment variables from DEVS.yml and merge with CLI --env flags
352
365
  devs_env = DevsConfigLoader.load_env_vars(dev_name, project.info.name)
353
366
  cli_env = parse_env_vars(env) if env else {}
354
367
  extra_env = merge_env_vars(devs_env, cli_env) if devs_env or cli_env else None
355
-
368
+
356
369
  if extra_env:
357
370
  console.print(f"🔧 Environment variables: {', '.join(f'{k}={v}' for k, v in extra_env.items())}")
358
-
371
+
359
372
  container_manager = ContainerManager(project, config)
360
373
  workspace_manager = WorkspaceManager(project, config)
361
-
374
+
362
375
  try:
363
376
  # Ensure workspace exists (handles live mode and reset internally)
364
377
  workspace_dir = workspace_manager.create_workspace(dev_name, reset_contents=reset_workspace, live=live)
365
378
  # Ensure container is running
366
379
  container_manager.ensure_container_running(
367
- dev_name=dev_name,
368
- workspace_dir=workspace_dir,
369
- force_rebuild=False,
370
- debug=debug,
371
- live=live,
380
+ dev_name=dev_name,
381
+ workspace_dir=workspace_dir,
382
+ force_rebuild=False,
383
+ debug=debug,
384
+ live=live,
372
385
  extra_env=extra_env
373
386
  )
374
-
387
+
375
388
  # Execute Claude
376
389
  console.print(f"🤖 Executing Claude in {dev_name}...")
377
390
  if reset_workspace and not live:
378
391
  console.print("🗑️ Workspace contents reset")
379
392
  console.print(f"📝 Prompt: {prompt}")
380
393
  console.print("")
381
-
394
+
382
395
  success, output, error = container_manager.exec_claude(
383
396
  dev_name=dev_name,
384
- workspace_dir=workspace_dir,
385
- prompt=prompt,
386
- debug=debug,
387
- stream=True,
388
- live=live,
397
+ workspace_dir=workspace_dir,
398
+ prompt=prompt,
399
+ debug=debug,
400
+ stream=True,
401
+ live=live,
389
402
  extra_env=extra_env
390
403
  )
391
-
404
+
392
405
  console.print("") # Add spacing after streamed output
393
406
  if success:
394
407
  console.print("✅ Claude execution completed")
@@ -399,197 +412,372 @@ def claude(dev_name: str, prompt: str, reset_workspace: bool, live: bool, env: t
399
412
  console.print("🚫 Error:")
400
413
  console.print(error)
401
414
  sys.exit(1)
402
-
415
+
403
416
  except (ContainerError, WorkspaceError) as e:
404
417
  console.print(f"❌ Error executing Claude in {dev_name}: {e}")
405
418
  sys.exit(1)
406
419
 
407
420
 
421
+ def _handle_claude_auth(api_key: str, debug: bool) -> None:
422
+ """Handle Claude authentication setup.
423
+
424
+ This configures Claude authentication that will be shared across
425
+ all devcontainers for this project. The authentication is stored
426
+ on the host and bind-mounted into containers.
427
+ """
428
+ try:
429
+ # Ensure Claude config directory exists
430
+ config.ensure_directories()
431
+
432
+ console.print("🔐 Setting up Claude authentication...")
433
+ console.print(f" Configuration will be saved to: {config.claude_config_dir}")
434
+
435
+ if api_key:
436
+ # Set API key directly using Claude CLI
437
+ console.print(" Using provided API key...")
438
+
439
+ # Set CLAUDE_CONFIG_DIR to our config directory and run auth with API key
440
+ env = os.environ.copy()
441
+ env['CLAUDE_CONFIG_DIR'] = str(config.claude_config_dir)
442
+
443
+ cmd = ['claude', 'auth', '--key', api_key]
444
+
445
+ if debug:
446
+ console.print(f"[dim]Running: {' '.join(cmd)}[/dim]")
447
+ console.print(f"[dim]CLAUDE_CONFIG_DIR: {config.claude_config_dir}[/dim]")
448
+
449
+ result = subprocess.run(
450
+ cmd,
451
+ env=env,
452
+ capture_output=True,
453
+ text=True
454
+ )
455
+
456
+ if result.returncode != 0:
457
+ error_msg = result.stderr or result.stdout or "Unknown error"
458
+ raise Exception(f"Claude authentication failed: {error_msg}")
459
+
460
+ else:
461
+ # Interactive authentication
462
+ console.print(" Starting interactive authentication...")
463
+ console.print(" Follow the prompts to authenticate with Claude")
464
+ console.print("")
465
+
466
+ # Set CLAUDE_CONFIG_DIR to our config directory
467
+ env = os.environ.copy()
468
+ env['CLAUDE_CONFIG_DIR'] = str(config.claude_config_dir)
469
+
470
+ cmd = ['claude', 'auth']
471
+
472
+ if debug:
473
+ console.print(f"[dim]Running: {' '.join(cmd)}[/dim]")
474
+ console.print(f"[dim]CLAUDE_CONFIG_DIR: {config.claude_config_dir}[/dim]")
475
+
476
+ # Run interactively
477
+ result = subprocess.run(
478
+ cmd,
479
+ env=env,
480
+ check=False
481
+ )
482
+
483
+ if result.returncode != 0:
484
+ raise Exception("Claude authentication was cancelled or failed")
485
+
486
+ console.print("")
487
+ console.print("✅ Claude authentication configured successfully!")
488
+ console.print(f" Configuration saved to: {config.claude_config_dir}")
489
+ console.print(" This authentication will be shared across all devcontainers")
490
+ console.print("")
491
+ console.print("💡 You can now use Claude in any devcontainer:")
492
+ console.print(" devs claude <dev-name> 'Your prompt here'")
493
+
494
+ except FileNotFoundError:
495
+ console.print("❌ Claude CLI not found on host machine")
496
+ console.print("")
497
+ console.print("Please install Claude CLI first:")
498
+ console.print(" npm install -g @anthropic-ai/claude-cli")
499
+ console.print("")
500
+ console.print("Note: Claude needs to be installed on the host machine")
501
+ console.print(" for authentication. It's already available in containers.")
502
+ sys.exit(1)
503
+
504
+ except Exception as e:
505
+ console.print(f"❌ Failed to configure Claude authentication: {e}")
506
+ if debug:
507
+ import traceback
508
+ console.print(traceback.format_exc())
509
+ sys.exit(1)
510
+
511
+
408
512
  @cli.command()
409
- @click.argument('dev_name')
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)')
410
517
  @click.option('--reset-workspace', is_flag=True, help='Reset workspace contents before execution')
411
518
  @click.option('--live', is_flag=True, help='Start container with current directory mounted as workspace')
412
519
  @click.option('--env', multiple=True, help='Environment variables to pass to container (format: VAR=value)')
413
520
  @debug_option
414
- def runtests(dev_name: str, reset_workspace: bool, live: bool, env: tuple, debug: bool) -> None:
415
- """Run tests in devcontainer.
416
-
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
+
417
524
  DEV_NAME: Development environment name
418
-
419
- Example: devs runtests sally
420
- Example: devs runtests sally --reset-workspace
421
- Example: devs runtests sally --live # Run with current directory
422
- Example: devs runtests sally --env NODE_ENV=test
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
423
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
+
424
543
  check_dependencies()
425
544
  project = get_project()
426
-
427
- # Load full DEVS configuration
428
- try:
429
- project_name = project.info.name
430
- except Exception:
431
- project_name = None
432
-
433
- devs_config = DevsConfigLoader.load(project_name)
434
-
435
- # Get test command from config
436
- command = devs_config.ci_test_command
437
-
545
+
438
546
  # Load environment variables from DEVS.yml and merge with CLI --env flags
439
- devs_env = devs_config.get_env_vars(dev_name)
547
+ devs_env = DevsConfigLoader.load_env_vars(dev_name, project.info.name)
440
548
  cli_env = parse_env_vars(env) if env else {}
441
549
  extra_env = merge_env_vars(devs_env, cli_env) if devs_env or cli_env else None
442
-
550
+
443
551
  if extra_env:
444
552
  console.print(f"🔧 Environment variables: {', '.join(f'{k}={v}' for k, v in extra_env.items())}")
445
-
553
+
446
554
  container_manager = ContainerManager(project, config)
447
555
  workspace_manager = WorkspaceManager(project, config)
448
-
556
+
449
557
  try:
450
558
  # Ensure workspace exists (handles live mode and reset internally)
451
559
  workspace_dir = workspace_manager.create_workspace(dev_name, reset_contents=reset_workspace, live=live)
452
560
  # Ensure container is running
453
561
  container_manager.ensure_container_running(
454
- dev_name=dev_name,
455
- workspace_dir=workspace_dir,
456
- force_rebuild=False,
457
- debug=debug,
458
- live=live,
562
+ dev_name=dev_name,
563
+ workspace_dir=workspace_dir,
564
+ force_rebuild=False,
565
+ debug=debug,
566
+ live=live,
459
567
  extra_env=extra_env
460
568
  )
461
-
462
- # Execute test command
463
- console.print(f"🧪 Running tests in {dev_name}...")
569
+
570
+ # Execute Codex
571
+ console.print(f"🤖 Executing Codex in {dev_name}...")
464
572
  if reset_workspace and not live:
465
573
  console.print("🗑️ Workspace contents reset")
466
- console.print(f"🔧 Command: {command}")
574
+ console.print(f"📝 Prompt: {prompt}")
467
575
  console.print("")
468
-
469
- success, output, error = container_manager.exec_command(
470
- dev_name=dev_name,
471
- workspace_dir=workspace_dir,
472
- command=command,
473
- debug=debug,
474
- stream=True,
475
- live=live,
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,
476
584
  extra_env=extra_env
477
585
  )
478
-
586
+
479
587
  console.print("") # Add spacing after streamed output
480
588
  if success:
481
- console.print("✅ Tests completed successfully")
589
+ console.print("✅ Codex execution completed")
482
590
  else:
483
- console.print("❌ Tests failed")
591
+ console.print("❌ Codex execution failed")
484
592
  if error:
485
593
  console.print("")
486
594
  console.print("🚫 Error:")
487
595
  console.print(error)
488
596
  sys.exit(1)
489
-
597
+
490
598
  except (ContainerError, WorkspaceError) as e:
491
- console.print(f"❌ Error running tests in {dev_name}: {e}")
599
+ console.print(f"❌ Error executing Codex in {dev_name}: {e}")
492
600
  sys.exit(1)
493
601
 
494
602
 
495
- @cli.command('claude-auth')
496
- @click.option('--api-key', help='Claude API key to authenticate with')
497
- @debug_option
498
- def claude_auth(api_key: str, debug: bool) -> None:
499
- """Set up Claude authentication for devcontainers.
500
-
501
- This configures Claude authentication that will be shared across
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
502
607
  all devcontainers for this project. The authentication is stored
503
608
  on the host and bind-mounted into containers.
504
-
505
- Example: devs claude-auth
506
- Example: devs claude-auth --api-key <YOUR_API_KEY>
507
609
  """
508
-
509
610
  try:
510
- # Ensure Claude config directory exists
611
+ # Ensure Codex config directory exists
511
612
  config.ensure_directories()
512
-
513
- console.print("🔐 Setting up Claude authentication...")
514
- console.print(f" Configuration will be saved to: {config.claude_config_dir}")
515
-
613
+
614
+ console.print("🔐 Setting up Codex authentication...")
615
+ console.print(f" Configuration will be saved to: {config.codex_config_dir}")
616
+
516
617
  if api_key:
517
- # Set API key directly using Claude CLI
618
+ # Set API key directly using Codex CLI
518
619
  console.print(" Using provided API key...")
519
-
520
- # Set CLAUDE_CONFIG_DIR to our config directory and run auth with API key
620
+
621
+ # Set CODEX_CONFIG_HOME to our config directory and run auth with API key
521
622
  env = os.environ.copy()
522
- env['CLAUDE_CONFIG_DIR'] = str(config.claude_config_dir)
523
-
524
- cmd = ['claude', 'auth', '--key', api_key]
525
-
623
+ env['CODEX_CONFIG_HOME'] = str(config.codex_config_dir)
624
+
625
+ cmd = ['codex', 'auth', '--api-key', api_key]
626
+
526
627
  if debug:
527
628
  console.print(f"[dim]Running: {' '.join(cmd)}[/dim]")
528
- console.print(f"[dim]CLAUDE_CONFIG_DIR: {config.claude_config_dir}[/dim]")
529
-
629
+ console.print(f"[dim]CODEX_CONFIG_HOME: {config.codex_config_dir}[/dim]")
630
+
530
631
  result = subprocess.run(
531
632
  cmd,
532
633
  env=env,
533
634
  capture_output=True,
534
635
  text=True
535
636
  )
536
-
637
+
537
638
  if result.returncode != 0:
538
639
  error_msg = result.stderr or result.stdout or "Unknown error"
539
- raise Exception(f"Claude authentication failed: {error_msg}")
540
-
640
+ raise Exception(f"Codex authentication failed: {error_msg}")
641
+
541
642
  else:
542
643
  # Interactive authentication
543
644
  console.print(" Starting interactive authentication...")
544
- console.print(" Follow the prompts to authenticate with Claude")
645
+ console.print(" Follow the prompts to authenticate with Codex")
545
646
  console.print("")
546
-
547
- # Set CLAUDE_CONFIG_DIR to our config directory
647
+
648
+ # Set CODEX_CONFIG_HOME to our config directory
548
649
  env = os.environ.copy()
549
- env['CLAUDE_CONFIG_DIR'] = str(config.claude_config_dir)
550
-
551
- cmd = ['claude', 'auth']
552
-
650
+ env['CODEX_CONFIG_HOME'] = str(config.codex_config_dir)
651
+
652
+ cmd = ['codex', 'auth']
653
+
553
654
  if debug:
554
655
  console.print(f"[dim]Running: {' '.join(cmd)}[/dim]")
555
- console.print(f"[dim]CLAUDE_CONFIG_DIR: {config.claude_config_dir}[/dim]")
556
-
656
+ console.print(f"[dim]CODEX_CONFIG_HOME: {config.codex_config_dir}[/dim]")
657
+
557
658
  # Run interactively
558
659
  result = subprocess.run(
559
660
  cmd,
560
661
  env=env,
561
662
  check=False
562
663
  )
563
-
664
+
564
665
  if result.returncode != 0:
565
- raise Exception("Claude authentication was cancelled or failed")
566
-
666
+ raise Exception("Codex authentication was cancelled or failed")
667
+
567
668
  console.print("")
568
- console.print("✅ Claude authentication configured successfully!")
569
- console.print(f" Configuration saved to: {config.claude_config_dir}")
669
+ console.print("✅ Codex authentication configured successfully!")
670
+ console.print(f" Configuration saved to: {config.codex_config_dir}")
570
671
  console.print(" This authentication will be shared across all devcontainers")
571
672
  console.print("")
572
- console.print("💡 You can now use Claude in any devcontainer:")
573
- console.print(" devs claude <dev-name> 'Your prompt here'")
574
-
673
+ console.print("💡 You can now use Codex in any devcontainer:")
674
+ console.print(" devs codex <dev-name> 'Your prompt here'")
675
+
575
676
  except FileNotFoundError:
576
- console.print("❌ Claude CLI not found on host machine")
677
+ console.print("❌ Codex CLI not found on host machine")
577
678
  console.print("")
578
- console.print("Please install Claude CLI first:")
579
- console.print(" npm install -g @anthropic-ai/claude-cli")
679
+ console.print("Please install Codex CLI first:")
680
+ console.print(" npm install -g @openai/codex")
580
681
  console.print("")
581
- console.print("Note: Claude needs to be installed on the host machine")
682
+ console.print("Note: Codex needs to be installed on the host machine")
582
683
  console.print(" for authentication. It's already available in containers.")
583
684
  sys.exit(1)
584
-
685
+
585
686
  except Exception as e:
586
- console.print(f"❌ Failed to configure Claude authentication: {e}")
687
+ console.print(f"❌ Failed to configure Codex authentication: {e}")
587
688
  if debug:
588
689
  import traceback
589
690
  console.print(traceback.format_exc())
590
691
  sys.exit(1)
591
692
 
592
693
 
694
+ @cli.command()
695
+ @click.argument('dev_name')
696
+ @click.option('--reset-workspace', is_flag=True, help='Reset workspace contents before execution')
697
+ @click.option('--live', is_flag=True, help='Start container with current directory mounted as workspace')
698
+ @click.option('--env', multiple=True, help='Environment variables to pass to container (format: VAR=value)')
699
+ @debug_option
700
+ def runtests(dev_name: str, reset_workspace: bool, live: bool, env: tuple, debug: bool) -> None:
701
+ """Run tests in devcontainer.
702
+
703
+ DEV_NAME: Development environment name
704
+
705
+ Example: devs runtests sally
706
+ Example: devs runtests sally --reset-workspace
707
+ Example: devs runtests sally --live # Run with current directory
708
+ Example: devs runtests sally --env NODE_ENV=test
709
+ """
710
+ check_dependencies()
711
+ project = get_project()
712
+
713
+ # Load full DEVS configuration
714
+ try:
715
+ project_name = project.info.name
716
+ except Exception:
717
+ project_name = None
718
+
719
+ devs_config = DevsConfigLoader.load(project_name)
720
+
721
+ # Get test command from config
722
+ command = devs_config.ci_test_command
723
+
724
+ # Load environment variables from DEVS.yml and merge with CLI --env flags
725
+ devs_env = devs_config.get_env_vars(dev_name)
726
+ cli_env = parse_env_vars(env) if env else {}
727
+ extra_env = merge_env_vars(devs_env, cli_env) if devs_env or cli_env else None
728
+
729
+ if extra_env:
730
+ console.print(f"🔧 Environment variables: {', '.join(f'{k}={v}' for k, v in extra_env.items())}")
731
+
732
+ container_manager = ContainerManager(project, config)
733
+ workspace_manager = WorkspaceManager(project, config)
734
+
735
+ try:
736
+ # Ensure workspace exists (handles live mode and reset internally)
737
+ workspace_dir = workspace_manager.create_workspace(dev_name, reset_contents=reset_workspace, live=live)
738
+ # Ensure container is running
739
+ container_manager.ensure_container_running(
740
+ dev_name=dev_name,
741
+ workspace_dir=workspace_dir,
742
+ force_rebuild=False,
743
+ debug=debug,
744
+ live=live,
745
+ extra_env=extra_env
746
+ )
747
+
748
+ # Execute test command
749
+ console.print(f"🧪 Running tests in {dev_name}...")
750
+ if reset_workspace and not live:
751
+ console.print("🗑️ Workspace contents reset")
752
+ console.print(f"🔧 Command: {command}")
753
+ console.print("")
754
+
755
+ success, output, error = container_manager.exec_command(
756
+ dev_name=dev_name,
757
+ workspace_dir=workspace_dir,
758
+ command=command,
759
+ debug=debug,
760
+ stream=True,
761
+ live=live,
762
+ extra_env=extra_env
763
+ )
764
+
765
+ console.print("") # Add spacing after streamed output
766
+ if success:
767
+ console.print("✅ Tests completed successfully")
768
+ else:
769
+ console.print("❌ Tests failed")
770
+ if error:
771
+ console.print("")
772
+ console.print("🚫 Error:")
773
+ console.print(error)
774
+ sys.exit(1)
775
+
776
+ except (ContainerError, WorkspaceError) as e:
777
+ console.print(f"❌ Error running tests in {dev_name}: {e}")
778
+ sys.exit(1)
779
+
780
+
593
781
  @cli.command()
594
782
  @click.option('--all-projects', is_flag=True, help='List containers for all projects')
595
783
  def list(all_projects: bool) -> None:
@@ -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.3
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
@@ -81,9 +81,9 @@ devs start frontend --env DEBUG=true --env API_URL=http://localhost:3000
81
81
  devs claude frontend "Fix the tests" --env NODE_ENV=test
82
82
 
83
83
  # Set up Claude authentication (once per host)
84
- devs claude-auth
84
+ devs claude --auth
85
85
  # Or with API key
86
- devs claude-auth --api-key <YOUR_API_KEY>
86
+ devs claude --auth --api-key <YOUR_API_KEY>
87
87
 
88
88
  # Clean up when done
89
89
  devs stop frontend backend
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devs-cli"
7
- version = "0.1.3"
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"
@@ -65,7 +65,7 @@ line-length = 88
65
65
  target-version = ['py38']
66
66
 
67
67
  [tool.mypy]
68
- python_version = "0.1.3"
68
+ python_version = "3.8"
69
69
  warn_return_any = true
70
70
  warn_unused_configs = true
71
71
  disallow_untyped_defs = true
@@ -123,31 +123,32 @@ class TestCLI:
123
123
  assert result.exit_code != 0
124
124
  assert "Missing argument" in result.output
125
125
 
126
- def test_claude_auth_command_help(self):
127
- """Test claude-auth command help."""
126
+ def test_claude_command_help(self):
127
+ """Test claude command help."""
128
128
  runner = CliRunner()
129
- result = runner.invoke(cli, ['claude-auth', '--help'])
130
-
129
+ result = runner.invoke(cli, ['claude', '--help'])
130
+
131
131
  assert result.exit_code == 0
132
- assert "Set up Claude authentication" in result.output
132
+ assert "Execute Claude CLI in devcontainer or set up authentication" in result.output
133
+ assert "--auth" in result.output
133
134
  assert "--api-key" in result.output
134
-
135
+
135
136
  @patch('devs.cli.subprocess.run')
136
137
  @patch('devs.cli.config')
137
138
  def test_claude_auth_with_api_key(self, mock_config, mock_subprocess):
138
- """Test claude-auth command with API key."""
139
+ """Test claude --auth command with API key."""
139
140
  # Setup mocks
140
141
  mock_config.claude_config_dir = '/tmp/test-claude-config'
141
142
  mock_config.ensure_directories = Mock()
142
143
  mock_subprocess.return_value.returncode = 0
143
-
144
+
144
145
  runner = CliRunner()
145
- result = runner.invoke(cli, ['claude-auth', '--api-key', 'test-key-123'])
146
-
146
+ result = runner.invoke(cli, ['claude', '--auth', '--api-key', 'test-key-123'])
147
+
147
148
  assert result.exit_code == 0
148
149
  assert "Setting up Claude authentication" in result.output
149
150
  assert "Claude authentication configured successfully" in result.output
150
-
151
+
151
152
  # Verify subprocess was called with correct arguments
152
153
  mock_subprocess.assert_called_once()
153
154
  call_args = mock_subprocess.call_args
@@ -155,43 +156,133 @@ class TestCLI:
155
156
  assert 'auth' in call_args[0][0]
156
157
  assert '--key' in call_args[0][0]
157
158
  assert 'test-key-123' in call_args[0][0]
158
-
159
+
159
160
  @patch('devs.cli.subprocess.run')
160
161
  @patch('devs.cli.config')
161
162
  def test_claude_auth_interactive(self, mock_config, mock_subprocess):
162
- """Test claude-auth command in interactive mode."""
163
+ """Test claude --auth command in interactive mode."""
163
164
  # Setup mocks
164
165
  mock_config.claude_config_dir = '/tmp/test-claude-config'
165
166
  mock_config.ensure_directories = Mock()
166
167
  mock_subprocess.return_value.returncode = 0
167
-
168
+
168
169
  runner = CliRunner()
169
- result = runner.invoke(cli, ['claude-auth'])
170
-
170
+ result = runner.invoke(cli, ['claude', '--auth'])
171
+
171
172
  assert result.exit_code == 0
172
173
  assert "Setting up Claude authentication" in result.output
173
174
  assert "Starting interactive authentication" in result.output
174
175
  assert "Claude authentication configured successfully" in result.output
175
-
176
+
176
177
  # Verify subprocess was called for interactive auth
177
178
  mock_subprocess.assert_called_once()
178
179
  call_args = mock_subprocess.call_args
179
180
  assert 'claude' in call_args[0][0]
180
181
  assert 'auth' in call_args[0][0]
181
182
  assert '--key' not in call_args[0][0]
182
-
183
+
183
184
  @patch('devs.cli.subprocess.run')
184
185
  @patch('devs.cli.config')
185
186
  def test_claude_auth_command_not_found(self, mock_config, mock_subprocess):
186
- """Test claude-auth when claude CLI is not installed."""
187
+ """Test claude --auth when claude CLI is not installed."""
187
188
  # Setup mocks
188
189
  mock_config.claude_config_dir = '/tmp/test-claude-config'
189
190
  mock_config.ensure_directories = Mock()
190
191
  mock_subprocess.side_effect = FileNotFoundError()
191
-
192
+
192
193
  runner = CliRunner()
193
- result = runner.invoke(cli, ['claude-auth'])
194
-
194
+ result = runner.invoke(cli, ['claude', '--auth'])
195
+
195
196
  assert result.exit_code == 1
196
197
  assert "Claude CLI not found" in result.output
197
- assert "npm install -g @anthropic-ai/claude-cli" in result.output
198
+ assert "npm install -g @anthropic-ai/claude-cli" in result.output
199
+
200
+ def test_claude_missing_args(self):
201
+ """Test claude command without required args (not using --auth)."""
202
+ runner = CliRunner()
203
+ result = runner.invoke(cli, ['claude'])
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
+
287
+ assert result.exit_code != 0
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