Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions lib/cli/src/crewai_cli/run_crew.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import os
from pathlib import Path
import re
import shlex
import subprocess
import sys
from typing import TYPE_CHECKING, Any, cast
Expand All @@ -17,6 +18,7 @@
build_env_with_all_tool_credentials,
enable_prompt_line_editing,
is_dmn_mode_enabled,
parse_toml,
read_toml,
)
from crewai_cli.version import get_crewai_version
Expand Down Expand Up @@ -604,12 +606,14 @@ def _run_flow_project(
run_declarative_flow_in_project_env(definition=definition)
return

_ensure_project_script(pyproject_data, "kickoff", entity_type="flow")
_execute_uv_script("kickoff", entity_type="flow")


def _run_classic_crew_project(
pyproject_data: dict[str, Any], trained_agents_file: str | None
) -> None:
_ensure_project_script(pyproject_data, "run_crew", entity_type="crew")
_execute_uv_script(
"run_crew",
entity_type="crew",
Expand All @@ -622,6 +626,75 @@ def _get_project_type(pyproject_data: dict[str, Any]) -> str | None:
return project_type if isinstance(project_type, str) else None


def _project_scripts(pyproject_data: dict[str, Any]) -> dict[str, Any]:
project = pyproject_data.get("project", {})
if not isinstance(project, dict):
return {}
scripts = project.get("scripts", {})
return scripts if isinstance(scripts, dict) else {}


def _ensure_project_script(
pyproject_data: dict[str, Any], script_name: str, *, entity_type: str
) -> None:
if script_name in _project_scripts(pyproject_data):
return

cwd = Path.cwd()
child_project_hint = _child_project_run_hint(script_name)
raise click.ClickException(
f"Cannot run this {entity_type} from {cwd}: pyproject.toml does not "
f"define the `{script_name}` project script.\n\n"
f"Run `crewai run` from a CrewAI {entity_type} project directory, or add "
f"`{script_name}` under `[project.scripts]` in pyproject.toml."
f"{child_project_hint}"
)


def _child_project_run_hint(script_name: str) -> str:
child_projects = _find_child_projects_with_script(script_name)
if not child_projects:
return ""

if len(child_projects) == 1:
project_path = _relative_child_project_path(child_projects[0])
project_command = shlex.quote(str(project_path))
return (
f"\n\nFound a CrewAI project in `{project_path}`. Try:\n\n"
f" cd {project_command}\n"
" crewai run"
)

project_list = "\n".join(
f" - `{_relative_child_project_path(project_path)}`"
for project_path in child_projects
)
return (
"\n\nFound CrewAI projects with the expected script:\n"
f"{project_list}\n\n"
"Run `crewai run` from one of those directories."
)


def _find_child_projects_with_script(script_name: str) -> list[Path]:
projects: list[Path] = []
for pyproject_path in sorted(Path.cwd().glob("*/pyproject.toml")):
try:
pyproject_data = parse_toml(pyproject_path.read_text())
except (OSError, ValueError):
continue
if script_name in _project_scripts(pyproject_data):
projects.append(pyproject_path.parent)
return projects


def _relative_child_project_path(project_path: Path) -> Path:
try:
return project_path.relative_to(Path.cwd())
except ValueError:
return project_path


def _warn_if_old_poetry_project(pyproject_data: dict[str, Any]) -> None:
crewai_version = get_crewai_version()
min_required_version = "0.71.0"
Expand Down
111 changes: 109 additions & 2 deletions lib/cli/tests/test_run_crew.py
Original file line number Diff line number Diff line change
Expand Up @@ -612,7 +612,10 @@ def test_run_crew_runs_classic_crew_project(monkeypatch, capsys):
monkeypatch.setattr(
run_crew_module,
"read_toml",
lambda: {"tool": {"crewai": {"type": "crew"}}},
lambda: {
"project": {"scripts": {"run_crew": "demo.main:run"}},
"tool": {"crewai": {"type": "crew"}},
},
)
monkeypatch.setattr(
run_crew_module,
Expand All @@ -631,14 +634,94 @@ def test_run_crew_runs_classic_crew_project(monkeypatch, capsys):
]


def test_run_crew_allows_legacy_project_with_run_script(monkeypatch):
calls = []

monkeypatch.setattr(run_crew_module, "_has_json_crew", lambda: False)
monkeypatch.setattr(
run_crew_module,
"read_toml",
lambda: {"project": {"scripts": {"run_crew": "demo.main:run"}}},
)
monkeypatch.setattr(
run_crew_module,
"_execute_uv_script",
lambda script_name, **kwargs: calls.append((script_name, kwargs)),
)

run_crew_module.run_crew()

assert calls == [
(
"run_crew",
{"entity_type": "crew", "trained_agents_file": None},
)
]


def test_run_crew_rejects_project_without_run_script(monkeypatch, tmp_path: Path):
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(run_crew_module, "_has_json_crew", lambda: False)
monkeypatch.setattr(
run_crew_module,
"read_toml",
lambda: {"project": {"name": "demo"}, "tool": {"crewai": {"type": "crew"}}},
)
monkeypatch.setattr(
run_crew_module,
"_execute_uv_script",
lambda *_args, **_kwargs: pytest.fail("missing script must not invoke uv"),
)

with pytest.raises(click.ClickException) as exc_info:
run_crew_module.run_crew()

message = exc_info.value.message
assert "does not define the `run_crew` project script" in message
assert "Run `crewai run` from a CrewAI crew project directory" in message


def test_run_crew_rejects_project_without_run_script_suggests_child_project(
monkeypatch, tmp_path: Path
):
monkeypatch.chdir(tmp_path)
child_project = tmp_path / "ai_agent_news"
child_project.mkdir()
(child_project / "pyproject.toml").write_text(
"[project.scripts]\nrun_crew = 'ai_agent_news.main:run'\n"
)
monkeypatch.setattr(run_crew_module, "_has_json_crew", lambda: False)
monkeypatch.setattr(
run_crew_module,
"read_toml",
lambda: {"project": {"name": "demo"}, "tool": {"crewai": {"type": "crew"}}},
)
monkeypatch.setattr(
run_crew_module,
"_execute_uv_script",
lambda *_args, **_kwargs: pytest.fail("missing script must not invoke uv"),
)

with pytest.raises(click.ClickException) as exc_info:
run_crew_module.run_crew()

message = exc_info.value.message
assert "Found a CrewAI project in `ai_agent_news`" in message
assert "cd ai_agent_news" in message
assert "crewai run" in message


def test_run_crew_runs_python_flow_project(monkeypatch, capsys):
calls = []

monkeypatch.setattr(run_crew_module, "_has_json_crew", lambda: False)
monkeypatch.setattr(
run_crew_module,
"read_toml",
lambda: {"tool": {"crewai": {"type": "flow"}}},
lambda: {
"project": {"scripts": {"kickoff": "demo.main:kickoff"}},
"tool": {"crewai": {"type": "flow"}},
},
)
monkeypatch.setattr(
run_crew_module,
Expand All @@ -652,6 +735,30 @@ def test_run_crew_runs_python_flow_project(monkeypatch, capsys):
assert calls == [("kickoff", {"entity_type": "flow"})]


def test_run_flow_rejects_project_without_kickoff_script(
monkeypatch, tmp_path: Path
):
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(run_crew_module, "_has_json_crew", lambda: False)
monkeypatch.setattr(
run_crew_module,
"read_toml",
lambda: {"project": {"name": "demo"}, "tool": {"crewai": {"type": "flow"}}},
)
monkeypatch.setattr(
run_crew_module,
"_execute_uv_script",
lambda *_args, **_kwargs: pytest.fail("missing script must not invoke uv"),
)

with pytest.raises(click.ClickException) as exc_info:
run_crew_module.run_crew()

message = exc_info.value.message
assert "does not define the `kickoff` project script" in message
assert "Run `crewai run` from a CrewAI flow project directory" in message


def test_run_crew_rejects_filename_for_flow_project(monkeypatch):
monkeypatch.setattr(run_crew_module, "_has_json_crew", lambda: False)
monkeypatch.setattr(
Expand Down