From fef32f43a0aeb3c23ff98d25337b0ed2507b1633 Mon Sep 17 00:00:00 2001 From: Vidit Ostwal <110953813+Vidit-Ostwal@users.noreply.github.com> Date: Wed, 5 Aug 2026 22:11:27 +0530 Subject: [PATCH] feat(cli): unify scaffolding under `crewai create ` (#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. --- docs/edge/en/concepts/cli.mdx | 59 ++++- docs/edge/en/concepts/skills.mdx | 12 +- .../edge/en/guides/coding-tools/agents-md.mdx | 6 +- lib/cli/src/crewai_cli/cli.py | 74 +++++- lib/cli/src/crewai_cli/utils.py | 9 + lib/cli/tests/test_cli.py | 1 + lib/cli/tests/test_create_unified.py | 230 ++++++++++++++++++ 7 files changed, 374 insertions(+), 17 deletions(-) create mode 100644 lib/cli/tests/test_create_unified.py diff --git a/docs/edge/en/concepts/cli.mdx b/docs/edge/en/concepts/cli.mdx index 831e32f11..a6825c6ba 100644 --- a/docs/edge/en/concepts/cli.mdx +++ b/docs/edge/en/concepts/cli.mdx @@ -36,24 +36,73 @@ crewai [COMMAND] [OPTIONS] [ARGUMENTS] ### 1. Create -Create a new crew or flow. +Create a new crew, flow, tool, skill, or template project. ```shell Terminal crewai create [OPTIONS] TYPE NAME ``` -- `TYPE`: Choose between "crew" or "flow" -- `NAME`: Name of the crew or flow +- `TYPE`: `crew`, `flow`, `tool`, `skill`, or `template` +- `NAME`: Name of the project, tool handle, skill, or template -Example: +#### Crew ```shell Terminal crewai create crew my_new_crew -crewai create flow my_new_flow +crewai create crew my_new_crew --classic ``` By default, `crewai create crew` creates a JSON-first crew project with `crew.jsonc` and `agents/*.jsonc`. Use `crewai create crew my_new_crew --classic` only when you want the older Python/YAML scaffold with `crew.py`, `config/agents.yaml`, and `config/tasks.yaml`. +#### Flow + +```shell Terminal +crewai create flow my_new_flow +crewai create flow my_new_flow --declarative +``` + +#### Tool + +Scaffold a custom tool repository: + +```shell Terminal +crewai create tool my_tool +``` + +#### Skill + +Scaffold an agent skill. Inside a crew project (where `pyproject.toml` exists), the skill is created under `./skills/`: + +```shell Terminal +crewai create skill my-skill +crewai create skill my-skill --no-project +``` + +Use `--no-project` to create the skill in the current directory instead of `./skills/`. + +#### Template + +Add a remote project template to the current directory: + +```shell Terminal +crewai create template my-template +crewai create template my-template --output-dir custom_dir +``` + +Use `--output-dir` to override the output folder name (defaults to the template name). + +#### Deprecated create aliases + +These older commands still work but print a yellow deprecation warning. Prefer the `crewai create ` forms above. + +| Deprecated | Use instead | +| :--- | :--- | +| `crewai tool create ` | `crewai create tool ` | +| `crewai skill create ` | `crewai create skill ` | +| `crewai template add ` | `crewai create template ` | + +Lifecycle commands are unchanged — for example `crewai tool install`, `crewai skill publish`, and `crewai template list` stay under their resource groups. + ### 2. Version Show the installed version of CrewAI. diff --git a/docs/edge/en/concepts/skills.mdx b/docs/edge/en/concepts/skills.mdx index d51fe8187..66960edb4 100644 --- a/docs/edge/en/concepts/skills.mdx +++ b/docs/edge/en/concepts/skills.mdx @@ -32,10 +32,10 @@ You often need **both**: skills for expertise, tools for action. They are config The CLI is the supported way to create a skill — it scaffolds the directory layout and a valid `SKILL.md` for you: ```shell Terminal -crewai skill create code-review +crewai create skill code-review ``` -Inside a crew project (where `pyproject.toml` lives) this creates `./skills/code-review/`; outside a project it creates `./code-review/` in the current directory (you can force that behavior with `--no-project`): +Inside a crew project (where `pyproject.toml` lives) this creates `./skills/code-review/`; outside a project it creates `./code-review/` in the current directory (you can force that behavior with `--no-project` on `crewai create skill`): ``` skills/ @@ -178,12 +178,16 @@ agent = Agent( ## Creating, Publishing, and Installing Skills -Skills have a full lifecycle managed by the CLI: **create them with `crewai skill create`, publish them with `crewai skill publish`** — hand-rolling directories works for local experiments, but the CLI is the intended workflow and keeps your skill layout and frontmatter valid. +Skills have a full lifecycle managed by the CLI: **create them with `crewai create skill`, publish them with `crewai skill publish`** — hand-rolling directories works for local experiments, but the CLI is the intended workflow and keeps your skill layout and frontmatter valid. + + + `crewai skill create` is deprecated and still works with a warning. Use `crewai create skill` instead. + ### Create ```shell Terminal -crewai skill create my-skill +crewai create skill my-skill ``` Scaffolds the directory (into `./skills/` inside a crew project) with a template `SKILL.md`, plus empty `scripts/`, `references/`, and `assets/` directories. Edit `SKILL.md` to define the instructions. diff --git a/docs/edge/en/guides/coding-tools/agents-md.mdx b/docs/edge/en/guides/coding-tools/agents-md.mdx index ea238c314..3736a7b6a 100644 --- a/docs/edge/en/guides/coding-tools/agents-md.mdx +++ b/docs/edge/en/guides/coding-tools/agents-md.mdx @@ -21,9 +21,13 @@ crewai create crew my_crew crewai create flow my_flow # Tool repository -crewai tool create my_tool +crewai create tool my_tool ``` + + `crewai tool create` is deprecated and still works with a warning. Use `crewai create tool` instead. + + ## Tool Setup: Point Assistants to AGENTS.md ### Codex diff --git a/lib/cli/src/crewai_cli/cli.py b/lib/cli/src/crewai_cli/cli.py index e4b0342c0..b08e5ac24 100644 --- a/lib/cli/src/crewai_cli/cli.py +++ b/lib/cli/src/crewai_cli/cli.py @@ -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 ` or `crewai create flow `." + "Use `crewai create ` 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) diff --git a/lib/cli/src/crewai_cli/utils.py b/lib/cli/src/crewai_cli/utils.py index 71206c8a4..e7517eb68 100644 --- a/lib/cli/src/crewai_cli/utils.py +++ b/lib/cli/src/crewai_cli/utils.py @@ -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_]*)}}") diff --git a/lib/cli/tests/test_cli.py b/lib/cli/tests/test_cli.py index 535daf7f2..438196ebb 100644 --- a/lib/cli/tests/test_cli.py +++ b/lib/cli/tests/test_cli.py @@ -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): diff --git a/lib/cli/tests/test_create_unified.py b/lib/cli/tests/test_create_unified.py new file mode 100644 index 000000000..4ca73f505 --- /dev/null +++ b/lib/cli/tests/test_create_unified.py @@ -0,0 +1,230 @@ +"""Tests for unified `crewai create ` 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 + )