devs-cli 3.2.3__tar.gz → 3.3.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.
Files changed (32) hide show
  1. {devs_cli-3.2.3/devs_cli.egg-info → devs_cli-3.3.0}/PKG-INFO +1 -1
  2. {devs_cli-3.2.3 → devs_cli-3.3.0}/devs/cli.py +37 -105
  3. {devs_cli-3.2.3 → devs_cli-3.3.0/devs_cli.egg-info}/PKG-INFO +1 -1
  4. {devs_cli-3.2.3 → devs_cli-3.3.0}/pyproject.toml +1 -1
  5. {devs_cli-3.2.3 → devs_cli-3.3.0}/tests/test_cli.py +4 -55
  6. {devs_cli-3.2.3 → devs_cli-3.3.0}/tests/test_container_manager.py +6 -5
  7. {devs_cli-3.2.3 → devs_cli-3.3.0}/LICENSE +0 -0
  8. {devs_cli-3.2.3 → devs_cli-3.3.0}/README.md +0 -0
  9. {devs_cli-3.2.3 → devs_cli-3.3.0}/devs/__init__.py +0 -0
  10. {devs_cli-3.2.3 → devs_cli-3.3.0}/devs/config.py +0 -0
  11. {devs_cli-3.2.3 → devs_cli-3.3.0}/devs/core/__init__.py +0 -0
  12. {devs_cli-3.2.3 → devs_cli-3.3.0}/devs/core/integration.py +0 -0
  13. {devs_cli-3.2.3 → devs_cli-3.3.0}/devs/exceptions.py +0 -0
  14. {devs_cli-3.2.3 → devs_cli-3.3.0}/devs/utils/__init__.py +0 -0
  15. {devs_cli-3.2.3 → devs_cli-3.3.0}/devs_cli.egg-info/SOURCES.txt +0 -0
  16. {devs_cli-3.2.3 → devs_cli-3.3.0}/devs_cli.egg-info/dependency_links.txt +0 -0
  17. {devs_cli-3.2.3 → devs_cli-3.3.0}/devs_cli.egg-info/entry_points.txt +0 -0
  18. {devs_cli-3.2.3 → devs_cli-3.3.0}/devs_cli.egg-info/requires.txt +0 -0
  19. {devs_cli-3.2.3 → devs_cli-3.3.0}/devs_cli.egg-info/top_level.txt +0 -0
  20. {devs_cli-3.2.3 → devs_cli-3.3.0}/setup.cfg +0 -0
  21. {devs_cli-3.2.3 → devs_cli-3.3.0}/tests/test_cli_clean.py +0 -0
  22. {devs_cli-3.2.3 → devs_cli-3.3.0}/tests/test_cli_misc.py +0 -0
  23. {devs_cli-3.2.3 → devs_cli-3.3.0}/tests/test_cli_start.py +0 -0
  24. {devs_cli-3.2.3 → devs_cli-3.3.0}/tests/test_cli_stop.py +0 -0
  25. {devs_cli-3.2.3 → devs_cli-3.3.0}/tests/test_cli_vscode.py +0 -0
  26. {devs_cli-3.2.3 → devs_cli-3.3.0}/tests/test_e2e.py +0 -0
  27. {devs_cli-3.2.3 → devs_cli-3.3.0}/tests/test_error_parsing.py +0 -0
  28. {devs_cli-3.2.3 → devs_cli-3.3.0}/tests/test_integration.py +0 -0
  29. {devs_cli-3.2.3 → devs_cli-3.3.0}/tests/test_live_mode.py +0 -0
  30. {devs_cli-3.2.3 → devs_cli-3.3.0}/tests/test_project.py +0 -0
  31. {devs_cli-3.2.3 → devs_cli-3.3.0}/tests/test_repo_cache.py +0 -0
  32. {devs_cli-3.2.3 → devs_cli-3.3.0}/tests/test_workspace_manager.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devs-cli
3
- Version: 3.2.3
3
+ Version: 3.3.0
4
4
  Summary: DevContainer Management Tool - Manage multiple named devcontainers for any project
5
5
  Author: Dan Lester
6
6
  License-Expression: MIT
@@ -11,7 +11,6 @@ from rich.console import Console
11
11
  from rich.table import Table
12
12
 
13
13
  from .config import config
14
- from pathlib import Path
15
14
  from .core import Project, ContainerManager, WorkspaceManager
16
15
  from .core.integration import VSCodeIntegration, ExternalToolIntegration
17
16
  from devs_common.devs_config import DevsConfigLoader
@@ -169,46 +168,52 @@ def cli(ctx, debug: bool, repo: str) -> None:
169
168
  @cli.command()
170
169
  @click.argument('dev_names', nargs=-1, required=True)
171
170
  @click.option('--rebuild', is_flag=True, help='Force rebuild of container images')
171
+ @click.option('--rebuild-if-changed', 'rebuild_if_changed', is_flag=True, help='Rebuild if devcontainer files have changed since last build')
172
172
  @click.option('--live', is_flag=True, help='Mount current directory as workspace instead of copying')
173
173
  @click.option('--env', multiple=True, help='Environment variables to pass to container (format: VAR=value)')
174
174
  @debug_option
175
- def start(dev_names: tuple, rebuild: bool, live: bool, env: tuple, debug: bool) -> None:
175
+ def start(dev_names: tuple, rebuild: bool, rebuild_if_changed: bool, live: bool, env: tuple, debug: bool) -> None:
176
176
  """Start named devcontainers.
177
-
177
+
178
178
  DEV_NAMES: One or more development environment names to start
179
-
179
+
180
180
  Example: devs start sally bob
181
181
  Example: devs start sally --live # Mount current directory directly
182
+ Example: devs start sally --rebuild-if-changed # Rebuild only if devcontainer files changed
182
183
  Example: devs start sally --env QUART_PORT=5001 --env DB_HOST=localhost:3307
183
184
  """
184
185
  check_dependencies()
185
186
  project = get_project()
186
-
187
+
187
188
  console.print(f"🚀 Starting devcontainers for project: {project.info.name}")
188
-
189
+
189
190
  container_manager = ContainerManager(project, config)
190
191
  workspace_manager = WorkspaceManager(project, config)
191
-
192
+
192
193
  for dev_name in dev_names:
193
194
  console.print(f" Starting: {dev_name}")
194
-
195
+
195
196
  # Load environment variables from DEVS.yml and merge with CLI --env flags
196
197
  devs_env = DevsConfigLoader.load_env_vars(dev_name, project.info.name)
197
198
  cli_env = parse_env_vars(env) if env else {}
198
199
  extra_env = merge_env_vars(devs_env, cli_env) if devs_env or cli_env else None
199
-
200
+
200
201
  if extra_env:
201
202
  console.print(f"🔧 Environment variables: {', '.join(f'{k}={v}' for k, v in extra_env.items())}")
202
-
203
+
203
204
  try:
204
205
  # Create/ensure workspace exists (handles live mode internally)
205
206
  workspace_dir = workspace_manager.create_workspace(dev_name, live=live)
206
-
207
- # Ensure container is running
207
+
208
+ # Ensure container is running.
209
+ # --rebuild: always force a full rebuild
210
+ # --rebuild-if-changed: rebuild only when devcontainer file content has changed
211
+ # (default): never auto-rebuild
208
212
  if container_manager.ensure_container_running(
209
- dev_name,
210
- workspace_dir,
213
+ dev_name,
214
+ workspace_dir,
211
215
  force_rebuild=rebuild,
216
+ check_rebuild=rebuild_if_changed,
212
217
  debug=debug,
213
218
  live=live,
214
219
  extra_env=extra_env
@@ -216,7 +221,7 @@ def start(dev_names: tuple, rebuild: bool, live: bool, env: tuple, debug: bool)
216
221
  continue
217
222
  else:
218
223
  console.print(f" ⚠️ Failed to start {dev_name}, continuing with others...")
219
-
224
+
220
225
  except (ContainerError, WorkspaceError) as e:
221
226
  console.print(f" ❌ Error starting {dev_name}: {e}")
222
227
  continue
@@ -269,8 +274,8 @@ def vscode(dev_names: tuple, delay: float, live: bool, env: tuple, debug: bool)
269
274
  # Ensure workspace exists (handles live mode internally)
270
275
  workspace_dir = workspace_manager.create_workspace(dev_name, live=live)
271
276
 
272
- # Ensure container is running before launching VS Code
273
- if container_manager.ensure_container_running(dev_name, workspace_dir, debug=debug, live=live, extra_env=extra_env):
277
+ # Ensure container is running before launching VS Code (no auto-rebuild in CLI)
278
+ if container_manager.ensure_container_running(dev_name, workspace_dir, check_rebuild=False, debug=debug, live=live, extra_env=extra_env):
274
279
  workspace_dirs.append(workspace_dir)
275
280
  valid_dev_names.append(dev_name)
276
281
  else:
@@ -362,13 +367,12 @@ def shell(dev_name: str, live: bool, env: tuple, debug: bool) -> None:
362
367
  @cli.command()
363
368
  @click.argument('dev_name', required=False)
364
369
  @click.argument('prompt', required=False)
365
- @click.option('--auth', is_flag=True, help='Set up Claude authentication for devcontainers')
366
- @click.option('--api-key', help='Claude API key to authenticate with (use with --auth)')
370
+ @click.option('--auth', is_flag=True, help='Show Claude authentication setup instructions')
367
371
  @click.option('--reset-workspace', is_flag=True, help='Reset workspace contents before execution')
368
372
  @click.option('--live', is_flag=True, help='Start container with current directory mounted as workspace')
369
373
  @click.option('--env', multiple=True, help='Environment variables to pass to container (format: VAR=value)')
370
374
  @debug_option
371
- def claude(dev_name: str, prompt: str, auth: bool, api_key: str, reset_workspace: bool, live: bool, env: tuple, debug: bool) -> None:
375
+ def claude(dev_name: str, prompt: str, auth: bool, reset_workspace: bool, live: bool, env: tuple, debug: bool) -> None:
372
376
  """Execute Claude CLI in devcontainer or set up authentication.
373
377
 
374
378
  DEV_NAME: Development environment name
@@ -378,12 +382,22 @@ def claude(dev_name: str, prompt: str, auth: bool, api_key: str, reset_workspace
378
382
  Example: devs claude sally "Fix the tests" --reset-workspace
379
383
  Example: devs claude sally "Fix the tests" --live # Run with current directory
380
384
  Example: devs claude sally "Start the server" --env QUART_PORT=5001
381
- Example: devs claude --auth # Interactive authentication
382
- Example: devs claude --auth --api-key <YOUR_KEY> # API key authentication
385
+ Example: devs claude --auth # Show auth setup instructions
383
386
  """
384
387
  # Handle authentication mode
385
388
  if auth:
386
- _handle_claude_auth(api_key=api_key, debug=debug)
389
+ console.print("🔐 Claude authentication for devcontainers")
390
+ console.print("")
391
+ console.print("1. Generate a token (on a machine with a browser):")
392
+ console.print(" [cyan]claude setup-token[/cyan]")
393
+ console.print("")
394
+ console.print("2. Add the token to your environment:")
395
+ console.print(" [cyan]export CLAUDE_CODE_OAUTH_TOKEN=<token>[/cyan]")
396
+ console.print("")
397
+ console.print(" Or add it to [cyan]~/.devs/envs/default/.env[/cyan]:")
398
+ console.print(" [dim]CLAUDE_CODE_OAUTH_TOKEN=<token>[/dim]")
399
+ console.print("")
400
+ console.print("The token will be automatically passed to all devcontainers.")
387
401
  return
388
402
 
389
403
  # Validate required arguments for execution mode
@@ -441,88 +455,6 @@ def claude(dev_name: str, prompt: str, auth: bool, api_key: str, reset_workspace
441
455
  sys.exit(1)
442
456
 
443
457
 
444
- def _handle_claude_auth(api_key: str, debug: bool) -> None:
445
- """Handle Claude authentication setup.
446
-
447
- This configures Claude authentication that will be shared across
448
- all devcontainers for this project. The authentication is stored
449
- on the host and bind-mounted into containers.
450
- """
451
- try:
452
- # Ensure Claude config directory exists
453
- config.ensure_directories()
454
-
455
- console.print("🔐 Setting up Claude authentication...")
456
- console.print(f" Configuration will be saved to: {config.claude_config_dir}")
457
-
458
- if api_key:
459
- # Set API key directly using Claude CLI
460
- console.print(" Using provided API key...")
461
-
462
- # Set CLAUDE_CONFIG_DIR to our config directory and run auth with API key
463
- env = os.environ.copy()
464
- env['CLAUDE_CONFIG_DIR'] = str(config.claude_config_dir)
465
-
466
- cmd = ['claude', 'auth', 'login', '--key', api_key]
467
-
468
- if debug:
469
- console.print(f"[dim]Running: {' '.join(cmd)}[/dim]")
470
- console.print(f"[dim]CLAUDE_CONFIG_DIR: {config.claude_config_dir}[/dim]")
471
-
472
- result = subprocess.run(
473
- cmd,
474
- env=env,
475
- capture_output=True,
476
- text=True
477
- )
478
-
479
- if result.returncode != 0:
480
- error_msg = result.stderr or result.stdout or "Unknown error"
481
- raise Exception(f"Claude authentication failed: {error_msg}")
482
-
483
- else:
484
- # Interactive authentication
485
- console.print(" Starting interactive authentication...")
486
- console.print(" Follow the prompts to authenticate with Claude")
487
- console.print("")
488
-
489
- # Set CLAUDE_CONFIG_DIR to our config directory
490
- env = os.environ.copy()
491
- env['CLAUDE_CONFIG_DIR'] = str(config.claude_config_dir)
492
-
493
- cmd = ['claude', 'auth', 'login']
494
-
495
- if debug:
496
- console.print(f"[dim]Running: {' '.join(cmd)}[/dim]")
497
- console.print(f"[dim]CLAUDE_CONFIG_DIR: {config.claude_config_dir}[/dim]")
498
-
499
- # Run interactively
500
- result = subprocess.run(
501
- cmd,
502
- env=env,
503
- check=False
504
- )
505
-
506
- if result.returncode != 0:
507
- raise Exception("Claude authentication was cancelled or failed")
508
-
509
- console.print("")
510
- console.print("✅ Claude authentication configured successfully!")
511
- console.print(f" Configuration saved to: {config.claude_config_dir}")
512
- console.print(" This authentication will be shared across all devcontainers")
513
- console.print("")
514
- console.print("💡 You can now use Claude in any devcontainer:")
515
- console.print(" devs claude <dev-name> 'Your prompt here'")
516
-
517
- except FileNotFoundError:
518
- console.print("❌ Claude CLI not found on host machine")
519
- console.print("")
520
- console.print("Please install Claude CLI first:")
521
- console.print(" npm install -g @anthropic-ai/claude-cli")
522
- console.print("")
523
- console.print("Note: Claude needs to be installed on the host machine")
524
- console.print(" for authentication. It's already available in containers.")
525
- sys.exit(1)
526
458
 
527
459
  except Exception as e:
528
460
  console.print(f"❌ Failed to configure Claude authentication: {e}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devs-cli
3
- Version: 3.2.3
3
+ Version: 3.3.0
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 = "3.2.3"
7
+ version = "3.3.0"
8
8
  description = "DevContainer Management Tool - Manage multiple named devcontainers for any project"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -131,66 +131,15 @@ class TestCLI:
131
131
  assert result.exit_code == 0
132
132
  assert "Execute Claude CLI in devcontainer or set up authentication" in result.output
133
133
  assert "--auth" in result.output
134
- assert "--api-key" in result.output
135
-
136
- @patch('devs.cli.subprocess.run')
137
- @patch('devs.cli.config')
138
- def test_claude_auth_with_api_key(self, mock_config, mock_subprocess):
139
- """Test claude --auth command with API key."""
140
- # Setup mocks
141
- mock_config.claude_config_dir = '/tmp/test-claude-config'
142
- mock_config.ensure_directories = Mock()
143
- mock_subprocess.return_value.returncode = 0
144
-
145
- runner = CliRunner()
146
- result = runner.invoke(cli, ['claude', '--auth', '--api-key', 'test-key-123'])
147
-
148
- assert result.exit_code == 0
149
- assert "Setting up Claude authentication" in result.output
150
- assert "Claude authentication configured successfully" in result.output
151
-
152
- # Verify subprocess was called with correct arguments
153
- mock_subprocess.assert_called_once()
154
- call_args = mock_subprocess.call_args
155
- assert call_args[0][0] == ['claude', 'auth', 'login', '--key', 'test-key-123']
156
-
157
- @patch('devs.cli.subprocess.run')
158
- @patch('devs.cli.config')
159
- def test_claude_auth_interactive(self, mock_config, mock_subprocess):
160
- """Test claude --auth command in interactive mode."""
161
- # Setup mocks
162
- mock_config.claude_config_dir = '/tmp/test-claude-config'
163
- mock_config.ensure_directories = Mock()
164
- mock_subprocess.return_value.returncode = 0
165
134
 
135
+ def test_claude_auth_shows_instructions(self):
136
+ """Test claude --auth shows setup instructions."""
166
137
  runner = CliRunner()
167
138
  result = runner.invoke(cli, ['claude', '--auth'])
168
139
 
169
140
  assert result.exit_code == 0
170
- assert "Setting up Claude authentication" in result.output
171
- assert "Starting interactive authentication" in result.output
172
- assert "Claude authentication configured successfully" in result.output
173
-
174
- # Verify subprocess was called for interactive auth
175
- mock_subprocess.assert_called_once()
176
- call_args = mock_subprocess.call_args
177
- assert call_args[0][0] == ['claude', 'auth', 'login']
178
-
179
- @patch('devs.cli.subprocess.run')
180
- @patch('devs.cli.config')
181
- def test_claude_auth_command_not_found(self, mock_config, mock_subprocess):
182
- """Test claude --auth when claude CLI is not installed."""
183
- # Setup mocks
184
- mock_config.claude_config_dir = '/tmp/test-claude-config'
185
- mock_config.ensure_directories = Mock()
186
- mock_subprocess.side_effect = FileNotFoundError()
187
-
188
- runner = CliRunner()
189
- result = runner.invoke(cli, ['claude', '--auth'])
190
-
191
- assert result.exit_code == 1
192
- assert "Claude CLI not found" in result.output
193
- assert "npm install -g @anthropic-ai/claude-cli" in result.output
141
+ assert "claude setup-token" in result.output
142
+ assert "CLAUDE_CODE_OAUTH_TOKEN" in result.output
194
143
 
195
144
  def test_claude_missing_args(self):
196
145
  """Test claude command without required args (not using --auth)."""
@@ -246,7 +246,7 @@ class TestContainerManager:
246
246
  assert result is False
247
247
 
248
248
  def test_should_rebuild_image_no_existing(self, mock_project):
249
- """Test should_rebuild_image when no image exists."""
249
+ """Test should_rebuild_image returns False when no existing container."""
250
250
  with patch('devs_common.utils.docker_client.docker') as mock_docker:
251
251
  mock_docker_instance = MagicMock()
252
252
  mock_docker.from_env.return_value = mock_docker_instance
@@ -257,13 +257,14 @@ class TestContainerManager:
257
257
 
258
258
  manager = ContainerManager(mock_project)
259
259
 
260
- # Mock no existing images
261
- manager.docker.find_images_by_pattern = MagicMock(return_value=[])
260
+ # Mock no existing containers
261
+ manager.docker.find_containers_by_labels = MagicMock(return_value=[])
262
262
 
263
- should_rebuild, reason = manager.should_rebuild_image("alice")
263
+ project_labels = {'devs.project': 'test-org-test-repo', 'devs.dev': 'alice'}
264
+ should_rebuild, reason = manager.should_rebuild_image("alice", project_labels)
264
265
 
265
266
  assert should_rebuild is False
266
- assert "No existing image" in reason
267
+ assert "No existing container" in reason
267
268
 
268
269
  def test_find_aborted_containers(self, mock_project):
269
270
  """Test finding aborted containers."""
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