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.
- {devs_cli-3.2.3/devs_cli.egg-info → devs_cli-3.3.0}/PKG-INFO +1 -1
- {devs_cli-3.2.3 → devs_cli-3.3.0}/devs/cli.py +37 -105
- {devs_cli-3.2.3 → devs_cli-3.3.0/devs_cli.egg-info}/PKG-INFO +1 -1
- {devs_cli-3.2.3 → devs_cli-3.3.0}/pyproject.toml +1 -1
- {devs_cli-3.2.3 → devs_cli-3.3.0}/tests/test_cli.py +4 -55
- {devs_cli-3.2.3 → devs_cli-3.3.0}/tests/test_container_manager.py +6 -5
- {devs_cli-3.2.3 → devs_cli-3.3.0}/LICENSE +0 -0
- {devs_cli-3.2.3 → devs_cli-3.3.0}/README.md +0 -0
- {devs_cli-3.2.3 → devs_cli-3.3.0}/devs/__init__.py +0 -0
- {devs_cli-3.2.3 → devs_cli-3.3.0}/devs/config.py +0 -0
- {devs_cli-3.2.3 → devs_cli-3.3.0}/devs/core/__init__.py +0 -0
- {devs_cli-3.2.3 → devs_cli-3.3.0}/devs/core/integration.py +0 -0
- {devs_cli-3.2.3 → devs_cli-3.3.0}/devs/exceptions.py +0 -0
- {devs_cli-3.2.3 → devs_cli-3.3.0}/devs/utils/__init__.py +0 -0
- {devs_cli-3.2.3 → devs_cli-3.3.0}/devs_cli.egg-info/SOURCES.txt +0 -0
- {devs_cli-3.2.3 → devs_cli-3.3.0}/devs_cli.egg-info/dependency_links.txt +0 -0
- {devs_cli-3.2.3 → devs_cli-3.3.0}/devs_cli.egg-info/entry_points.txt +0 -0
- {devs_cli-3.2.3 → devs_cli-3.3.0}/devs_cli.egg-info/requires.txt +0 -0
- {devs_cli-3.2.3 → devs_cli-3.3.0}/devs_cli.egg-info/top_level.txt +0 -0
- {devs_cli-3.2.3 → devs_cli-3.3.0}/setup.cfg +0 -0
- {devs_cli-3.2.3 → devs_cli-3.3.0}/tests/test_cli_clean.py +0 -0
- {devs_cli-3.2.3 → devs_cli-3.3.0}/tests/test_cli_misc.py +0 -0
- {devs_cli-3.2.3 → devs_cli-3.3.0}/tests/test_cli_start.py +0 -0
- {devs_cli-3.2.3 → devs_cli-3.3.0}/tests/test_cli_stop.py +0 -0
- {devs_cli-3.2.3 → devs_cli-3.3.0}/tests/test_cli_vscode.py +0 -0
- {devs_cli-3.2.3 → devs_cli-3.3.0}/tests/test_e2e.py +0 -0
- {devs_cli-3.2.3 → devs_cli-3.3.0}/tests/test_error_parsing.py +0 -0
- {devs_cli-3.2.3 → devs_cli-3.3.0}/tests/test_integration.py +0 -0
- {devs_cli-3.2.3 → devs_cli-3.3.0}/tests/test_live_mode.py +0 -0
- {devs_cli-3.2.3 → devs_cli-3.3.0}/tests/test_project.py +0 -0
- {devs_cli-3.2.3 → devs_cli-3.3.0}/tests/test_repo_cache.py +0 -0
- {devs_cli-3.2.3 → devs_cli-3.3.0}/tests/test_workspace_manager.py +0 -0
|
@@ -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='
|
|
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,
|
|
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 #
|
|
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
|
-
|
|
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}")
|
|
@@ -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 "
|
|
171
|
-
assert "
|
|
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
|
|
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
|
|
261
|
-
manager.docker.
|
|
260
|
+
# Mock no existing containers
|
|
261
|
+
manager.docker.find_containers_by_labels = MagicMock(return_value=[])
|
|
262
262
|
|
|
263
|
-
|
|
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
|
|
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
|
|
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
|