From ef9ade38cb33dad736268d432cb9b90e8e0f86d3 Mon Sep 17 00:00:00 2001 From: Vlad Cimpeanu Date: Thu, 17 Sep 2026 17:48:29 +0300 Subject: [PATCH 1/5] fix(cli): stop `uipath init` on an unscaffolded Studio Web project Studio Web's cloud-workspace flow creates a folder holding only `project.uiproj`. Running `uipath init` there wrote `.env`, `uipath.json`, `bindings.json` and `entry-points.json` before crashing on the missing `pyproject.toml`, leaving the project half-initialised. `init` now checks for that state before writing anything and exits with an actionable message pointing at `uipath new `, where the name is taken from `project.uiproj` and normalized to a valid project name. Co-Authored-By: Claude Fable 5.1 --- packages/uipath/pyproject.toml | 2 +- packages/uipath/src/uipath/_cli/cli_init.py | 44 ++++++++++++++ packages/uipath/tests/cli/test_init.py | 67 +++++++++++++++++++++ packages/uipath/uv.lock | 2 +- 4 files changed, 113 insertions(+), 2 deletions(-) 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..b7c0ff0ba 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,47 @@ 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. + + Lowercases the name and collapses any run of characters outside + ``[a-z0-9._-]`` into a single hyphen (PEP 508 name rules). + """ + 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 still an unscaffolded Studio Web template. + + Studio Web's cloud-workspace flow creates a folder holding only + ``project.uiproj``. ``uipath init`` only discovers an existing project, so + running it in that state would leave half-initialised config files behind. + Instead, stop before writing anything and point the user at ``uipath new``. + + Hidden entries (``.env``, ``.git``, ``.uipath``, ...) are ignored when + deciding whether the directory holds anything besides ``project.uiproj``. + """ + visible_entries = [ + entry for entry in os.listdir(current_directory) if not entry.startswith(".") + ] + if visible_entries != [UIPROJ_FILE]: + return + + project_name = "" + 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 + + new_command = f"uipath new {project_name or ''}" + console.error( + f"This project has not been scaffolded yet: '{UIPROJ_FILE}' is the only file in this directory.\n" + f"Scaffold it first by running:\n`{new_command}`" + ) + + def write_uiproj_file( entry_point_schemas: list[UiPathRuntimeSchema], current_directory: str, @@ -424,6 +467,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..e12b4a960 100644 --- a/packages/uipath/tests/cli/test_init.py +++ b/packages/uipath/tests/cli/test_init.py @@ -609,6 +609,73 @@ 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_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" }, From 902bc6392844406236b4e986e07d087221811264 Mon Sep 17 00:00:00 2001 From: Vlad Cimpeanu Date: Thu, 17 Sep 2026 17:52:29 +0300 Subject: [PATCH 2/5] chore(cli): trim _to_project_name docstring Co-Authored-By: Claude Fable 5.1 --- packages/uipath/src/uipath/_cli/cli_init.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/uipath/src/uipath/_cli/cli_init.py b/packages/uipath/src/uipath/_cli/cli_init.py index b7c0ff0ba..57fbcb957 100644 --- a/packages/uipath/src/uipath/_cli/cli_init.py +++ b/packages/uipath/src/uipath/_cli/cli_init.py @@ -210,11 +210,7 @@ def write_entry_points_file(entry_points: list[UiPathRuntimeSchema]) -> Path: def _to_project_name(raw_name: str) -> str: - """Normalize a display name into a valid ``pyproject.toml`` project name. - - Lowercases the name and collapses any run of characters outside - ``[a-z0-9._-]`` into a single hyphen (PEP 508 name rules). - """ + """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("-._") From 2c5bec3576cfaaea93ac8889498e1698fd85abb7 Mon Sep 17 00:00:00 2001 From: Vlad Cimpeanu Date: Thu, 17 Sep 2026 17:53:12 +0300 Subject: [PATCH 3/5] chore(cli): trim ensure_project_is_scaffolded docstring Co-Authored-By: Claude Fable 5.1 --- packages/uipath/src/uipath/_cli/cli_init.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/packages/uipath/src/uipath/_cli/cli_init.py b/packages/uipath/src/uipath/_cli/cli_init.py index 57fbcb957..e7d30ae96 100644 --- a/packages/uipath/src/uipath/_cli/cli_init.py +++ b/packages/uipath/src/uipath/_cli/cli_init.py @@ -216,16 +216,7 @@ def _to_project_name(raw_name: str) -> str: def ensure_project_is_scaffolded(current_directory: str) -> None: - """Abort when the directory is still an unscaffolded Studio Web template. - - Studio Web's cloud-workspace flow creates a folder holding only - ``project.uiproj``. ``uipath init`` only discovers an existing project, so - running it in that state would leave half-initialised config files behind. - Instead, stop before writing anything and point the user at ``uipath new``. - - Hidden entries (``.env``, ``.git``, ``.uipath``, ...) are ignored when - deciding whether the directory holds anything besides ``project.uiproj``. - """ + """Abort when the directory is still an unscaffolded Studio Web template.""" visible_entries = [ entry for entry in os.listdir(current_directory) if not entry.startswith(".") ] From cab71f4ccc4fbf2ebe10da1c81946a3d05207a2c Mon Sep 17 00:00:00 2001 From: Vlad Cimpeanu Date: Fri, 18 Sep 2026 09:51:03 +0300 Subject: [PATCH 4/5] fix(cli): also stop `uipath init` in an empty directory Co-Authored-By: Claude Fable 5.1 --- packages/uipath/src/uipath/_cli/cli_init.py | 24 +++++++++++-------- packages/uipath/tests/cli/test_init.py | 26 +++++++++++++++++++++ 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/packages/uipath/src/uipath/_cli/cli_init.py b/packages/uipath/src/uipath/_cli/cli_init.py index e7d30ae96..741c0e528 100644 --- a/packages/uipath/src/uipath/_cli/cli_init.py +++ b/packages/uipath/src/uipath/_cli/cli_init.py @@ -216,23 +216,27 @@ def _to_project_name(raw_name: str) -> str: def ensure_project_is_scaffolded(current_directory: str) -> None: - """Abort when the directory is still an unscaffolded Studio Web template.""" - visible_entries = [ + """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(".") - ] - if visible_entries != [UIPROJ_FILE]: + } + if not visible_entries <= {UIPROJ_FILE}: return project_name = "" - 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 + if visible_entries: + 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: + reason = "this directory is empty." new_command = f"uipath new {project_name or ''}" console.error( - f"This project has not been scaffolded yet: '{UIPROJ_FILE}' is the only file in this directory.\n" + f"This project has not been scaffolded yet: {reason}\n" f"Scaffold it first by running:\n`{new_command}`" ) diff --git a/packages/uipath/tests/cli/test_init.py b/packages/uipath/tests/cli/test_init.py index e12b4a960..bef7727a4 100644 --- a/packages/uipath/tests/cli/test_init.py +++ b/packages/uipath/tests/cli/test_init.py @@ -645,6 +645,32 @@ def test_init_aborts_on_unscaffolded_project_without_name( 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: From 7c4504e8cf5b558c6c7d0541811a6288b68d01fd Mon Sep 17 00:00:00 2001 From: Vlad Cimpeanu Date: Thu, 1 Oct 2026 12:05:20 +0300 Subject: [PATCH 5/5] refactor(cli): make scaffold guard branches explicit Co-Authored-By: Claude Fable 5.1 --- packages/uipath/src/uipath/_cli/cli_init.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/uipath/src/uipath/_cli/cli_init.py b/packages/uipath/src/uipath/_cli/cli_init.py index 741c0e528..b45a505f9 100644 --- a/packages/uipath/src/uipath/_cli/cli_init.py +++ b/packages/uipath/src/uipath/_cli/cli_init.py @@ -220,11 +220,11 @@ def ensure_project_is_scaffolded(current_directory: str) -> None: visible_entries = { entry for entry in os.listdir(current_directory) if not entry.startswith(".") } - if not visible_entries <= {UIPROJ_FILE}: - return project_name = "" - if visible_entries: + 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: @@ -232,7 +232,7 @@ def ensure_project_is_scaffolded(current_directory: str) -> None: except (OSError, json.JSONDecodeError): pass else: - reason = "this directory is empty." + return new_command = f"uipath new {project_name or ''}" console.error(