diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 141da3925..ae581401d 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.14.28" +version = "2.14.29" description = "Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools." readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath/src/uipath/_cli/cli_init.py b/packages/uipath/src/uipath/_cli/cli_init.py index d6ff01a90..b45a505f9 100644 --- a/packages/uipath/src/uipath/_cli/cli_init.py +++ b/packages/uipath/src/uipath/_cli/cli_init.py @@ -4,6 +4,7 @@ import json import logging import os +import re import shutil import uuid from pathlib import Path @@ -28,6 +29,7 @@ ENTRY_POINTS_FILE, PYTHON_CONFIGURATION_FILE, UIPATH_CONFIG_FILE, + UIPROJ_FILE, ) from uipath.runtime import ( UiPathRuntimeContext, @@ -207,6 +209,38 @@ def write_entry_points_file(entry_points: list[UiPathRuntimeSchema]) -> Path: return entry_points_file_path +def _to_project_name(raw_name: str) -> str: + """Normalize a display name into a valid ``pyproject.toml`` project name.""" + normalized = re.sub(r"[^a-z0-9._-]+", "-", raw_name.strip().lower()) + return normalized.strip("-._") + + +def ensure_project_is_scaffolded(current_directory: str) -> None: + """Abort when the directory is empty or still an unscaffolded Studio Web template.""" + visible_entries = { + entry for entry in os.listdir(current_directory) if not entry.startswith(".") + } + + project_name = "" + if not visible_entries: + reason = "this directory is empty." + elif visible_entries == {UIPROJ_FILE}: + reason = f"'{UIPROJ_FILE}' is the only file in this directory." + try: + with open(os.path.join(current_directory, UIPROJ_FILE), "r") as f: + project_name = _to_project_name(str(json.load(f).get("Name") or "")) + except (OSError, json.JSONDecodeError): + pass + else: + return + + new_command = f"uipath new {project_name or ''}" + console.error( + f"This project has not been scaffolded yet: {reason}\n" + f"Scaffold it first by running:\n`{new_command}`" + ) + + def write_uiproj_file( entry_point_schemas: list[UiPathRuntimeSchema], current_directory: str, @@ -424,6 +458,7 @@ def init(no_agents_md_override: bool) -> None: """Initialize the project.""" with console.spinner("Initializing UiPath project ..."): current_directory = os.getcwd() + ensure_project_is_scaffolded(current_directory) generate_env_file(current_directory) async def initialize() -> list[UiPathRuntimeSchema]: diff --git a/packages/uipath/tests/cli/test_init.py b/packages/uipath/tests/cli/test_init.py index b38725124..bef7727a4 100644 --- a/packages/uipath/tests/cli/test_init.py +++ b/packages/uipath/tests/cli/test_init.py @@ -609,6 +609,99 @@ def test_init_creates_uiproj_with_function_type( assert uiproj["Description"] == "Test" assert uiproj["MainFile"] is None + def test_init_aborts_on_unscaffolded_studio_web_project( + self, runner: CliRunner, temp_dir: str + ) -> None: + """Test that init stops before writing anything when only project.uiproj exists.""" + with runner.isolated_filesystem(temp_dir=temp_dir): + with open("project.uiproj", "w") as f: + json.dump( + { + "Name": "My Function", + "ProjectType": "Function", + "MainFile": None, + }, + f, + ) + + result = runner.invoke(cli, ["init"], env={}) + + assert result.exit_code == 1 + assert "has not been scaffolded yet" in result.output + assert "uipath new my-function" in result.output + assert sorted(os.listdir(".")) == ["project.uiproj"] + + def test_init_aborts_on_unscaffolded_project_without_name( + self, runner: CliRunner, temp_dir: str + ) -> None: + """Test that init falls back to a placeholder name when uiproj has none.""" + with runner.isolated_filesystem(temp_dir=temp_dir): + with open("project.uiproj", "w") as f: + json.dump({"ProjectType": "Function"}, f) + + result = runner.invoke(cli, ["init"], env={}) + + assert result.exit_code == 1 + assert "uipath new " in result.output + assert sorted(os.listdir(".")) == ["project.uiproj"] + + def test_init_aborts_on_empty_directory( + self, runner: CliRunner, temp_dir: str + ) -> None: + """Test that init stops before writing anything in an empty directory.""" + with runner.isolated_filesystem(temp_dir=temp_dir): + result = runner.invoke(cli, ["init"], env={}) + + assert result.exit_code == 1 + assert "this directory is empty" in result.output + assert "uipath new " in result.output + assert os.listdir(".") == [] + + def test_init_aborts_on_unscaffolded_project_with_malformed_uiproj( + self, runner: CliRunner, temp_dir: str + ) -> None: + """Test that an unreadable project.uiproj still yields the placeholder hint.""" + with runner.isolated_filesystem(temp_dir=temp_dir): + with open("project.uiproj", "w") as f: + f.write("{ not json") + + result = runner.invoke(cli, ["init"], env={}) + + assert result.exit_code == 1 + assert "uipath new " in result.output + assert sorted(os.listdir(".")) == ["project.uiproj"] + + def test_init_ignores_hidden_entries_when_detecting_unscaffolded_project( + self, runner: CliRunner, temp_dir: str + ) -> None: + """Test that hidden files do not hide the unscaffolded Studio Web state.""" + with runner.isolated_filesystem(temp_dir=temp_dir): + with open("project.uiproj", "w") as f: + json.dump({"Name": "Function", "ProjectType": "Function"}, f) + os.makedirs(".uipath") + with open(".env", "w") as f: + f.write("") + + result = runner.invoke(cli, ["init"], env={}) + + assert result.exit_code == 1 + assert "uipath new function" in result.output + + def test_init_proceeds_when_uiproj_has_siblings( + self, runner: CliRunner, temp_dir: str + ) -> None: + """Test that an existing project.uiproj alongside sources does not trigger the guard.""" + with runner.isolated_filesystem(temp_dir=temp_dir): + with open("project.uiproj", "w") as f: + json.dump({"Name": "my-project", "ProjectType": "Function"}, f) + self._generate_pyproject("my-project") + + result = runner.invoke(cli, ["init"], env={}) + + assert result.exit_code == 0 + assert "has not been scaffolded yet" not in result.output + assert "Updated 'project.uiproj' file" in result.output + def test_init_creates_uiproj_with_agent_type( self, runner: CliRunner, temp_dir: str ) -> None: diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index fe23bda33..0f3825838 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2599,7 +2599,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.14.28" +version = "2.14.29" source = { editable = "." } dependencies = [ { name = "applicationinsights" },