mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-08-10 08:21:54 +00:00
feat(cli): unify scaffolding under crewai create <resource> (#6821)
* feat(cli): add canonical `crewai create tool` command Unify tool scaffolding under the create verb and deprecate `crewai tool create` with a yellow warning while keeping backward compatibility. * feat(cli): add canonical `crewai create skill` command Unify skill scaffolding under the create verb and deprecate `crewai skill create` with a yellow warning while keeping backward compatibility. * feat(cli): add canonical `crewai create template` command Unify template scaffolding under the create verb and deprecate `crewai template add` with a yellow warning while keeping backward compatibility. * docs: document unified `crewai create` scaffolding commands Document canonical create forms for tool, skill, and template projects, note deprecated aliases, and update skills and agents-md guides. * feat(cli): extend create picker and DMN guidance for all types Show tool, skill, and template in the interactive create picker and list every supported type in the CREWAI_DMN usage error. * fix(cli): allow create tool/skill/template in CREWAI_DMN mode Only set skip_provider in DMN mode for crew creation, since tool, skill, and template paths reject that flag as a crew-only option. * test(cli): patch TemplateCommand at cli lookup site in DMN test create() resolves TemplateCommand from crewai_cli.cli, not from remote_template.main directly.
This commit is contained in:
@@ -19,6 +19,7 @@ from crewai_cli.utils import (
|
||||
enable_prompt_line_editing,
|
||||
is_dmn_mode_enabled,
|
||||
read_toml,
|
||||
warn_deprecated_command,
|
||||
)
|
||||
|
||||
|
||||
@@ -137,7 +138,10 @@ def uv(uv_args: tuple[str, ...]) -> None:
|
||||
|
||||
@crewai.command()
|
||||
@click.argument(
|
||||
"type", required=False, default=None, type=click.Choice(["crew", "flow"])
|
||||
"type",
|
||||
required=False,
|
||||
default=None,
|
||||
type=click.Choice(["crew", "flow", "tool", "skill", "template"]),
|
||||
)
|
||||
@click.argument("name", required=False, default=None)
|
||||
@click.option("--provider", type=str, help="The provider to use for the crew")
|
||||
@@ -152,6 +156,21 @@ def uv(uv_args: tuple[str, ...]) -> None:
|
||||
is_flag=True,
|
||||
help="Create a declarative Flow project instead of a Python Flow project",
|
||||
)
|
||||
@click.option(
|
||||
"--no-project",
|
||||
"in_project",
|
||||
is_flag=True,
|
||||
default=True,
|
||||
flag_value=False,
|
||||
help="Skill only: create in current dir instead of ./skills/",
|
||||
)
|
||||
@click.option(
|
||||
"-o",
|
||||
"--output-dir",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Template only: directory name for the template (defaults to template name)",
|
||||
)
|
||||
def create(
|
||||
type: str | None,
|
||||
name: str | None,
|
||||
@@ -159,14 +178,17 @@ def create(
|
||||
skip_provider: bool = False,
|
||||
classic: bool = False,
|
||||
declarative: bool = False,
|
||||
in_project: bool = True,
|
||||
output_dir: str | None = None,
|
||||
) -> None:
|
||||
"""Create a new crew, or flow."""
|
||||
"""Create a new crew, flow, tool, skill, or template."""
|
||||
dmn_mode = is_dmn_mode_enabled()
|
||||
if not type:
|
||||
if dmn_mode:
|
||||
raise click.UsageError(
|
||||
"TYPE is required when CREWAI_DMN is set. "
|
||||
"Use `crewai create crew <name>` or `crewai create flow <name>`."
|
||||
"Use `crewai create <type> <name>` where type is one of: "
|
||||
"crew, flow, tool, skill, template."
|
||||
)
|
||||
from crewai_cli.tui_picker import pick
|
||||
|
||||
@@ -176,6 +198,9 @@ def create(
|
||||
"flow",
|
||||
"A deterministic workflow with full control over agents and crews",
|
||||
),
|
||||
("tool", "A custom tool for the CrewAI Tool Repository"),
|
||||
("skill", "An agent skill with instructions and optional assets"),
|
||||
("template", "A remote project template from the CrewAI gallery"),
|
||||
]
|
||||
type = pick("What would you like to create?", options)
|
||||
if type is None:
|
||||
@@ -189,9 +214,36 @@ def create(
|
||||
click.style(f" Name of your {type}", fg="cyan", bold=True),
|
||||
prompt_suffix=click.style(" › ", fg="bright_white"), # noqa: RUF001
|
||||
)
|
||||
if dmn_mode:
|
||||
if dmn_mode and type == "crew":
|
||||
skip_provider = True
|
||||
if type == "crew":
|
||||
if not in_project and type != "skill":
|
||||
raise click.UsageError("--no-project can only be used with skill projects.")
|
||||
if output_dir is not None and type != "template":
|
||||
raise click.UsageError("--output-dir can only be used with template projects.")
|
||||
if type == "tool":
|
||||
if declarative or classic or provider is not None or skip_provider:
|
||||
raise click.UsageError(
|
||||
"Crew and flow options cannot be used with tool projects."
|
||||
)
|
||||
from crewai_cli.tools.main import ToolCommand
|
||||
|
||||
ToolCommand().create(name)
|
||||
elif type == "skill":
|
||||
if declarative or classic or provider is not None or skip_provider:
|
||||
raise click.UsageError(
|
||||
"Crew and flow options cannot be used with skill projects."
|
||||
)
|
||||
from crewai_cli.skills.main import SkillCommand
|
||||
|
||||
SkillCommand().create(name, in_project=in_project)
|
||||
elif type == "template":
|
||||
if declarative or classic or provider is not None or skip_provider:
|
||||
raise click.UsageError(
|
||||
"Crew and flow options cannot be used with template projects."
|
||||
)
|
||||
template_cmd = TemplateCommand()
|
||||
template_cmd.add_template(name, output_dir)
|
||||
elif type == "crew":
|
||||
if declarative:
|
||||
raise click.UsageError("--declarative can only be used with flow projects")
|
||||
if classic:
|
||||
@@ -207,7 +259,10 @@ def create(
|
||||
|
||||
create_flow(name, declarative=declarative)
|
||||
else:
|
||||
click.secho("Error: Invalid type. Must be 'crew' or 'flow'.", fg="red")
|
||||
click.secho(
|
||||
"Error: Invalid type. Must be 'crew', 'flow', 'tool', 'skill', or 'template'.",
|
||||
fg="red",
|
||||
)
|
||||
|
||||
|
||||
@crewai.command()
|
||||
@@ -652,6 +707,8 @@ def tool() -> None:
|
||||
@tool.command(name="create")
|
||||
@click.argument("handle")
|
||||
def tool_create(handle: str) -> None:
|
||||
"""[Deprecated: use `crewai create tool`] Create a custom tool project."""
|
||||
warn_deprecated_command(old="crewai tool create", new="crewai create tool")
|
||||
from crewai_cli.tools.main import ToolCommand
|
||||
|
||||
tool_cmd = ToolCommand()
|
||||
@@ -702,6 +759,8 @@ def skill() -> None:
|
||||
help="Create skill in current dir instead of ./skills/",
|
||||
)
|
||||
def skill_create(name: str, in_project: bool) -> None:
|
||||
"""[Deprecated: use `crewai create skill`] Create a new agent skill."""
|
||||
warn_deprecated_command(old="crewai skill create", new="crewai create skill")
|
||||
from crewai_cli.skills.main import SkillCommand
|
||||
|
||||
skill_cmd = SkillCommand()
|
||||
@@ -765,7 +824,8 @@ def template_list() -> None:
|
||||
help="Directory name for the template (defaults to template name)",
|
||||
)
|
||||
def template_add(name: str, output_dir: str | None) -> None:
|
||||
"""Add a template to the current directory."""
|
||||
"""[Deprecated: use `crewai create template`] Add a template to the current directory."""
|
||||
warn_deprecated_command(old="crewai template add", new="crewai create template")
|
||||
template_cmd = TemplateCommand()
|
||||
template_cmd.add_template(name, output_dir)
|
||||
|
||||
|
||||
@@ -44,10 +44,19 @@ __all__ = [
|
||||
"render_template",
|
||||
"tree_copy",
|
||||
"tree_find_and_replace",
|
||||
"warn_deprecated_command",
|
||||
"write_env_file",
|
||||
]
|
||||
|
||||
|
||||
def warn_deprecated_command(*, old: str, new: str) -> None:
|
||||
"""Print a yellow deprecation warning for a legacy CLI command path."""
|
||||
click.secho(
|
||||
f"Warning: The command '{old}' is deprecated. Use '{new}' instead.",
|
||||
fg="yellow",
|
||||
)
|
||||
|
||||
|
||||
console = Console()
|
||||
_TEMPLATE_TOKEN_RE = re.compile(r"{{([a-zA-Z_][a-zA-Z0-9_]*)}}")
|
||||
|
||||
|
||||
@@ -228,6 +228,7 @@ def test_create_requires_type_in_dmn_mode(runner):
|
||||
|
||||
assert result.exit_code == 2
|
||||
assert "TYPE is required when CREWAI_DMN is set" in result.output
|
||||
assert "crew, flow, tool, skill, template" in result.output
|
||||
|
||||
|
||||
def test_create_requires_name_in_dmn_mode(runner):
|
||||
|
||||
230
lib/cli/tests/test_create_unified.py
Normal file
230
lib/cli/tests/test_create_unified.py
Normal file
@@ -0,0 +1,230 @@
|
||||
"""Tests for unified `crewai create <resource>` scaffolding commands."""
|
||||
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
|
||||
from crewai_cli.cli import create, crewai
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def runner():
|
||||
return CliRunner()
|
||||
|
||||
|
||||
@mock.patch("crewai_cli.tools.main.ToolCommand")
|
||||
def test_create_tool_invokes_tool_command(mock_tool_command_cls, runner):
|
||||
result = runner.invoke(create, ["tool", "my_tool"])
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
mock_tool_command_cls.return_value.create.assert_called_once_with("my_tool")
|
||||
assert "deprecated" not in result.output.lower()
|
||||
|
||||
|
||||
@mock.patch("crewai_cli.tools.main.ToolCommand")
|
||||
def test_tool_create_is_deprecated_and_still_works(mock_tool_command_cls, runner):
|
||||
result = runner.invoke(crewai, ["tool", "create", "my_tool"])
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
mock_tool_command_cls.return_value.create.assert_called_once_with("my_tool")
|
||||
assert (
|
||||
"Warning: The command 'crewai tool create' is deprecated. "
|
||||
"Use 'crewai create tool' instead."
|
||||
in result.output
|
||||
)
|
||||
|
||||
|
||||
@mock.patch("crewai_cli.tools.main.ToolCommand")
|
||||
@pytest.mark.parametrize("extra_args", [["--classic"], ["--declarative"], ["--provider", "openai"], ["--skip_provider"]])
|
||||
def test_create_tool_rejects_crew_and_flow_flags(mock_tool_command_cls, runner, extra_args):
|
||||
result = runner.invoke(create, ["tool", "my_tool", *extra_args])
|
||||
|
||||
assert result.exit_code == 2, result.output
|
||||
assert "Crew and flow options cannot be used with tool projects." in result.output
|
||||
mock_tool_command_cls.return_value.create.assert_not_called()
|
||||
|
||||
|
||||
@mock.patch("crewai_cli.skills.main.SkillCommand")
|
||||
def test_create_skill_invokes_skill_command(mock_skill_command_cls, runner):
|
||||
result = runner.invoke(create, ["skill", "my-skill"])
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
mock_skill_command_cls.return_value.create.assert_called_once_with(
|
||||
"my-skill", in_project=True
|
||||
)
|
||||
assert "deprecated" not in result.output.lower()
|
||||
|
||||
|
||||
@mock.patch("crewai_cli.skills.main.SkillCommand")
|
||||
def test_create_skill_no_project_flag(mock_skill_command_cls, runner):
|
||||
result = runner.invoke(create, ["skill", "my-skill", "--no-project"])
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
mock_skill_command_cls.return_value.create.assert_called_once_with(
|
||||
"my-skill", in_project=False
|
||||
)
|
||||
|
||||
|
||||
@mock.patch("crewai_cli.skills.main.SkillCommand")
|
||||
def test_skill_create_is_deprecated_and_still_works(mock_skill_command_cls, runner):
|
||||
result = runner.invoke(crewai, ["skill", "create", "my-skill"])
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
mock_skill_command_cls.return_value.create.assert_called_once_with(
|
||||
"my-skill", in_project=True
|
||||
)
|
||||
assert (
|
||||
"Warning: The command 'crewai skill create' is deprecated. "
|
||||
"Use 'crewai create skill' instead."
|
||||
in result.output
|
||||
)
|
||||
|
||||
|
||||
@mock.patch("crewai_cli.skills.main.SkillCommand")
|
||||
@pytest.mark.parametrize(
|
||||
"extra_args",
|
||||
[["--classic"], ["--declarative"], ["--provider", "openai"], ["--skip_provider"]],
|
||||
)
|
||||
def test_create_skill_rejects_crew_and_flow_flags(
|
||||
mock_skill_command_cls, runner, extra_args
|
||||
):
|
||||
result = runner.invoke(create, ["skill", "my-skill", *extra_args])
|
||||
|
||||
assert result.exit_code == 2, result.output
|
||||
assert "Crew and flow options cannot be used with skill projects." in result.output
|
||||
mock_skill_command_cls.return_value.create.assert_not_called()
|
||||
|
||||
|
||||
@mock.patch("crewai_cli.skills.main.SkillCommand")
|
||||
def test_create_crew_rejects_no_project_flag(mock_skill_command_cls, runner):
|
||||
result = runner.invoke(create, ["crew", "my-crew", "--no-project"])
|
||||
|
||||
assert result.exit_code == 2, result.output
|
||||
assert "--no-project can only be used with skill projects." in result.output
|
||||
mock_skill_command_cls.return_value.create.assert_not_called()
|
||||
|
||||
|
||||
@mock.patch("crewai_cli.remote_template.main.TemplateCommand")
|
||||
def test_create_template_invokes_template_command(mock_template_command_cls, runner):
|
||||
result = runner.invoke(create, ["template", "my-template"])
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
mock_template_command_cls.return_value.add_template.assert_called_once_with(
|
||||
"my-template", None
|
||||
)
|
||||
assert "deprecated" not in result.output.lower()
|
||||
|
||||
|
||||
@mock.patch("crewai_cli.remote_template.main.TemplateCommand")
|
||||
def test_create_template_output_dir_flag(mock_template_command_cls, runner):
|
||||
result = runner.invoke(
|
||||
create, ["template", "my-template", "--output-dir", "custom_dir"]
|
||||
)
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
mock_template_command_cls.return_value.add_template.assert_called_once_with(
|
||||
"my-template", "custom_dir"
|
||||
)
|
||||
|
||||
|
||||
@mock.patch("crewai_cli.remote_template.main.TemplateCommand")
|
||||
def test_template_add_is_deprecated_and_still_works(mock_template_command_cls, runner):
|
||||
result = runner.invoke(
|
||||
crewai, ["template", "add", "my-template", "--output-dir", "custom_dir"]
|
||||
)
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
mock_template_command_cls.return_value.add_template.assert_called_once_with(
|
||||
"my-template", "custom_dir"
|
||||
)
|
||||
assert (
|
||||
"Warning: The command 'crewai template add' is deprecated. "
|
||||
"Use 'crewai create template' instead."
|
||||
in result.output
|
||||
)
|
||||
|
||||
|
||||
@mock.patch("crewai_cli.remote_template.main.TemplateCommand")
|
||||
@pytest.mark.parametrize(
|
||||
"extra_args",
|
||||
[["--classic"], ["--declarative"], ["--provider", "openai"], ["--skip_provider"]],
|
||||
)
|
||||
def test_create_template_rejects_crew_and_flow_flags(
|
||||
mock_template_command_cls, runner, extra_args
|
||||
):
|
||||
result = runner.invoke(create, ["template", "my-template", *extra_args])
|
||||
|
||||
assert result.exit_code == 2, result.output
|
||||
assert (
|
||||
"Crew and flow options cannot be used with template projects."
|
||||
in result.output
|
||||
)
|
||||
mock_template_command_cls.return_value.add_template.assert_not_called()
|
||||
|
||||
|
||||
@mock.patch("crewai_cli.remote_template.main.TemplateCommand")
|
||||
def test_create_crew_rejects_output_dir_flag(mock_template_command_cls, runner):
|
||||
result = runner.invoke(create, ["crew", "my-crew", "--output-dir", "custom_dir"])
|
||||
|
||||
assert result.exit_code == 2, result.output
|
||||
assert "--output-dir can only be used with template projects." in result.output
|
||||
mock_template_command_cls.return_value.add_template.assert_not_called()
|
||||
|
||||
|
||||
@mock.patch("crewai_cli.cli.enable_prompt_line_editing")
|
||||
@mock.patch("crewai_cli.cli.click.prompt", return_value="picked-tool")
|
||||
@mock.patch("crewai_cli.tui_picker.pick", return_value="tool")
|
||||
@mock.patch("crewai_cli.tools.main.ToolCommand")
|
||||
def test_create_picker_supports_tool_skill_and_template(
|
||||
mock_tool_command_cls,
|
||||
mock_pick,
|
||||
mock_prompt,
|
||||
mock_enable_prompt,
|
||||
runner,
|
||||
):
|
||||
result = runner.invoke(create, [])
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
mock_pick.assert_called_once()
|
||||
picker_options = mock_pick.call_args[0][1]
|
||||
assert {option[0] for option in picker_options} == {
|
||||
"crew",
|
||||
"flow",
|
||||
"tool",
|
||||
"skill",
|
||||
"template",
|
||||
}
|
||||
mock_prompt.assert_called_once()
|
||||
mock_tool_command_cls.return_value.create.assert_called_once_with("picked-tool")
|
||||
|
||||
|
||||
_DMN_ENV = {"CREWAI_DMN": "True"}
|
||||
|
||||
|
||||
@mock.patch("crewai_cli.tools.main.ToolCommand")
|
||||
def test_create_tool_works_in_dmn_mode(mock_tool_command_cls, runner):
|
||||
result = runner.invoke(create, ["tool", "my_tool"], env=_DMN_ENV)
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
mock_tool_command_cls.return_value.create.assert_called_once_with("my_tool")
|
||||
|
||||
|
||||
@mock.patch("crewai_cli.skills.main.SkillCommand")
|
||||
def test_create_skill_works_in_dmn_mode(mock_skill_command_cls, runner):
|
||||
result = runner.invoke(create, ["skill", "my-skill"], env=_DMN_ENV)
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
mock_skill_command_cls.return_value.create.assert_called_once_with(
|
||||
"my-skill", in_project=True
|
||||
)
|
||||
|
||||
|
||||
@mock.patch("crewai_cli.cli.TemplateCommand")
|
||||
def test_create_template_works_in_dmn_mode(mock_template_command_cls, runner):
|
||||
result = runner.invoke(create, ["template", "my-template"], env=_DMN_ENV)
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
mock_template_command_cls.return_value.add_template.assert_called_once_with(
|
||||
"my-template", None
|
||||
)
|
||||
Reference in New Issue
Block a user