diff --git a/src/dspy_cli/commands/new.py b/src/dspy_cli/commands/new.py index dc81e80..39c1607 100644 --- a/src/dspy_cli/commands/new.py +++ b/src/dspy_cli/commands/new.py @@ -51,7 +51,12 @@ default=None, help="LiteLLM model string (e.g., anthropic/claude-sonnet-4-5, openai/gpt-4o)", ) -def new(project_name, program_name, signature, module_type, model): +@click.option( + "--api-key", + default=None, + help="API key for the LLM provider (will be stored in .env)", +) +def new(project_name, program_name, signature, module_type, model, api_key): """Create a new DSPy project with boilerplate structure. Creates a directory with PROJECT_NAME and sets up a complete @@ -162,8 +167,13 @@ def new(project_name, program_name, signature, module_type, model): detected_key, env_var_name = detect_api_key(provider) api_key_env_var = env_var_name - # Prompt for API key (will ask to confirm if detected, or enter new one) - api_key_value = prompt_api_key(provider_display, env_var_name, detected_key) + # Use provided API key or prompt for one + if api_key is not None: + # API key was provided via CLI + api_key_value = api_key + else: + # Prompt for API key (will ask to confirm if detected, or enter new one) + api_key_value = prompt_api_key(provider_display, env_var_name, detected_key) else: # For local models, optionally prompt for api_base click.echo(click.style(f"Detected local model provider: {provider_display}", fg="green")) diff --git a/tests/test_commands_smoke.py b/tests/test_commands_smoke.py index 82448e6..aced8d0 100644 --- a/tests/test_commands_smoke.py +++ b/tests/test_commands_smoke.py @@ -26,13 +26,29 @@ def tmp_cwd(tmp_path, monkeypatch): os.chdir(old) +def with_new_defaults(args): + """Add default args to 'new' command to make it non-interactive.""" + out = args[:] + if "--program-name" not in out and "-p" not in out: + out += ["--program-name", "main"] + if "--module-type" not in out and "-m" not in out: + out += ["--module-type", "Predict"] + if "--signature" not in out and "-s" not in out: + out += ["--signature", "question -> answer"] + if "--model" not in out: + out += ["--model", "openai/gpt-4o-mini"] + if "--api-key" not in out: + out += ["--api-key", ""] # Empty string to skip API key prompt + return out + + def test_cli_e2e_smoke(runner, tmp_cwd, monkeypatch): """End-to-end smoke test: new -> generate -> serve. Creates project, generates components, validates serve args. """ # 1. Test 'new' command - res = runner.invoke(main, ["new", "acme-app"], catch_exceptions=False) + res = runner.invoke(main, with_new_defaults(["new", "acme-app"]), catch_exceptions=False) assert res.exit_code == 0 proj = tmp_cwd / "acme-app" @@ -42,8 +58,8 @@ def test_cli_e2e_smoke(runner, tmp_cwd, monkeypatch): assert (proj / name).exists(), f"Missing {name}" # Verify code structure created - assert (proj / "src" / "acme_app" / "modules" / "acme_app_predict.py").exists() - assert (proj / "src" / "acme_app" / "signatures" / "acme_app.py").exists() + assert (proj / "src" / "acme_app" / "modules" / "main_predict.py").exists() + assert (proj / "src" / "acme_app" / "signatures" / "main.py").exists() assert (proj / "tests" / "test_modules.py").exists() # 2. Test 'generate scaffold' command @@ -114,7 +130,7 @@ def test_new_with_signature(runner, tmp_cwd): """Test 'new' command with custom signature.""" res = runner.invoke( main, - ["new", "my-project", "-p", "analyzer", "-s", "text, context: list[str] -> summary"], + with_new_defaults(["new", "my-project", "-p", "analyzer", "-s", "text, context: list[str] -> summary"]), catch_exceptions=False ) assert res.exit_code == 0 @@ -133,7 +149,7 @@ def test_new_with_signature(runner, tmp_cwd): def test_generate_different_module_types(runner, tmp_cwd): """Test generating different module types.""" # Create a project first - res = runner.invoke(main, ["new", "test-app"], catch_exceptions=False) + res = runner.invoke(main, with_new_defaults(["new", "test-app"]), catch_exceptions=False) assert res.exit_code == 0 proj = tmp_cwd / "test-app" diff --git a/tests/test_serve_integration.py b/tests/test_serve_integration.py index e65c6c9..3b2d6aa 100644 --- a/tests/test_serve_integration.py +++ b/tests/test_serve_integration.py @@ -38,15 +38,20 @@ def temp_project(tmp_path, monkeypatch): # Change to temp directory monkeypatch.chdir(tmp_path) - # Use actual 'dspy-cli new' command to create project + # Use actual 'dspy-cli new' command to create project (with defaults to avoid prompts) runner = CliRunner() - result = runner.invoke(main, ["new", "testpkg"], catch_exceptions=False) + result = runner.invoke( + main, + ["new", "testpkg", "--program-name", "main", "--module-type", "Predict", + "--signature", "question -> answer", "--model", "openai/gpt-4o-mini", "--api-key", ""], + catch_exceptions=False + ) assert result.exit_code == 0 project_root = tmp_path / "testpkg" # Remove the default module and add our no-LLM Echo module - default_module = project_root / "src" / "testpkg" / "modules" / "testpkg_predict.py" + default_module = project_root / "src" / "testpkg" / "modules" / "main_predict.py" if default_module.exists(): default_module.unlink()