From 0ceaab0913ba5532db562b94bccc0b1825a1bce5 Mon Sep 17 00:00:00 2001 From: SimoneBottoni Date: Fri, 25 Sep 2026 12:05:45 +0200 Subject: [PATCH 1/2] chore: add --org-id-auto option to init command for organization selection --- docs/getting_started/cli.md | 3 ++- src/sourcerykit/cli/init.py | 21 +++++++++++++--- src/sourcerykit/cli/main.py | 5 +++- tests/unit/test_cli.py | 48 +++++++++++++++++++++++++++++++++++++ 4 files changed, 72 insertions(+), 5 deletions(-) diff --git a/docs/getting_started/cli.md b/docs/getting_started/cli.md index e5fd5b0..5d4f763 100644 --- a/docs/getting_started/cli.md +++ b/docs/getting_started/cli.md @@ -53,7 +53,7 @@ Global config is shared across all projects. Local config is project-specific an Setup wizard for browser login (OAuth), database linking, and project initialization. ```bash -sourcerykit init [--postgres-url URL] [--project-name NAME] [--sandbox] +sourcerykit init [--postgres-url URL] [--project-name NAME] [--sandbox] [--org-id-auto] ``` **Options:** @@ -62,6 +62,7 @@ sourcerykit init [--postgres-url URL] [--project-name NAME] [--sandbox] | `--postgres-url` | Full `postgresql://` URL | | `--project-name` | Project name | | `--sandbox` | Use a hosted sandbox database instead of your own PostgreSQL | +| `--org-id-auto` | If you belong to several organizations, reuse the saved one (or the lowest ID) instead of prompting. Needed for non-interactive runs in that case | > [!NOTE] > Login is **browser-based OAuth** (PKCE). Passing any flag opens the browser login once, diff --git a/src/sourcerykit/cli/init.py b/src/sourcerykit/cli/init.py index 44fcd76..36f59f5 100644 --- a/src/sourcerykit/cli/init.py +++ b/src/sourcerykit/cli/init.py @@ -41,6 +41,7 @@ def _run_oauth_browser( postgres_url: str | None = None, project_name: str | None = None, sandbox: bool = False, + org_id_auto: bool = False, ) -> None: """OAuth2 browser login, then run the post-auth setup phases. @@ -66,6 +67,7 @@ def _run_oauth_browser( postgres_url=postgres_url, project_name=project_name, sandbox=sandbox, + org_id_auto=org_id_auto, ): console.print("\nšŸ‘‹ Setup closed. Happy coding!") raise typer.Exit() @@ -140,6 +142,7 @@ def _execute_post_auth_phases( postgres_url: str | None = None, project_name: str | None = None, sandbox: bool = False, + org_id_auto: bool = False, ) -> bool: """Executes organisation, database, project, bootstrap, and saving steps.""" @@ -187,6 +190,14 @@ def _execute_post_auth_phases( elif len(orgs) == 1: org_id = str(orgs[0]["id"]) + elif org_id_auto: + # Keep the saved org, else a stable pick. + ids = sorted(str(o["id"]) for o in orgs) + saved = str(load_app_dir_config().get("org_id", "")) + org_id = saved if saved in ids else ids[0] + elif not sys.stdin.isatty(): + console.print("[red]āŒ You belong to multiple organizations; rerun with --org-id-auto.[/red]") + return False else: console.print("\n[bold]šŸ¢ Choose your organization workspace[/bold]") choices = [{"name": f"{o.get('name', o['id'])} ({o['id']})", "value": str(o["id"])} for o in orgs] @@ -286,12 +297,15 @@ def config_provably( postgres_url: str | None = None, project_name: str | None = None, sandbox: bool = False, + org_id_auto: bool = False, ) -> None: console.print(logo.print_logo(), "\n\n") # Non-interactive: any flag implies a browser login, then continue with the flags. - if postgres_url or project_name or sandbox: - _run_oauth_browser(postgres_url=postgres_url, project_name=project_name, sandbox=sandbox) + if postgres_url or project_name or sandbox or org_id_auto: + _run_oauth_browser( + postgres_url=postgres_url, project_name=project_name, sandbox=sandbox, org_id_auto=org_id_auto + ) return while True: @@ -326,6 +340,7 @@ def config_provably( postgres_url=postgres_url, project_name=project_name, sandbox=sandbox, + org_id_auto=org_id_auto, ): console.print("\nšŸ‘‹ Setup closed. Happy coding!") raise typer.Exit() @@ -347,4 +362,4 @@ def config_provably( console.print("\nšŸ‘‹ Setup closed. Happy coding!") return - _run_oauth_browser(sandbox=sandbox) + _run_oauth_browser(sandbox=sandbox, org_id_auto=org_id_auto) diff --git a/src/sourcerykit/cli/main.py b/src/sourcerykit/cli/main.py index 4db26cc..45b4716 100644 --- a/src/sourcerykit/cli/main.py +++ b/src/sourcerykit/cli/main.py @@ -25,8 +25,11 @@ def init( postgres_url: str | None = typer.Option(None, "--postgres-url", help="full postgres:// URL"), project_name: str | None = typer.Option(None, "--project-name", help="project name"), sandbox: bool = typer.Option(False, "--sandbox", help="use hosted sandbox database"), + org_id_auto: bool = typer.Option( + False, "--org-id-auto", help="with several organizations, reuse the saved one or pick one without prompting" + ), ) -> None: - config_provably(postgres_url=postgres_url, project_name=project_name, sandbox=sandbox) + config_provably(postgres_url=postgres_url, project_name=project_name, sandbox=sandbox, org_id_auto=org_id_auto) @app.command(help="validate configuration and connectivity") diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py index 468b031..1c56f7e 100644 --- a/tests/unit/test_cli.py +++ b/tests/unit/test_cli.py @@ -7,6 +7,7 @@ from provably import OAuthTokens, ProvablyConnectionError from sourcerykit.cli.init import ( + _execute_post_auth_phases, _run_oauth_browser, ) from sourcerykit.cli.utils import ( @@ -132,6 +133,7 @@ def test_logs_in_and_runs_post_auth_with_flags(self) -> None: postgres_url="postgresql://u:p@h:5432/db", project_name="proj", sandbox=True, + org_id_auto=False, ) def test_returns_without_phases_on_connection_error(self) -> None: @@ -145,6 +147,52 @@ def test_returns_without_phases_on_connection_error(self) -> None: mock_phases.assert_not_called() +class TestExecutePostAuthPhasesOrgs: + _A = "11111111-1111-1111-1111-111111111111" + _B = "22222222-2222-2222-2222-222222222222" + _C = "33333333-3333-3333-3333-333333333333" + + def _picked_org(self, saved_config: dict[str, str]) -> str: + orgs = [{"id": self._C}, {"id": self._A}, {"id": self._B}] + with ( + patch("sourcerykit.cli.init.service.get_organizations", new=AsyncMock(return_value=orgs)), + patch("sourcerykit.cli.init.load_app_dir_config", return_value=saved_config), + patch("sourcerykit.cli.init.questionary") as mock_q, + patch("sourcerykit.cli.init.save_app_dir_config") as mock_save, + patch("sourcerykit.cli.init.provably_service.create_sandbox", new=AsyncMock(side_effect=Exception("stop"))), + patch("sourcerykit.cli.init.console"), + ): + _execute_post_auth_phases("at", email="u@example.com", sandbox=True, org_id_auto=True) + + mock_q.select.assert_not_called() + return str(mock_save.call_args.kwargs["org_id"]) + + def test_multiple_orgs_keeps_saved_org(self) -> None: + assert self._picked_org({"org_id": self._B}) == self._B + + def test_multiple_orgs_picks_lowest_id_without_saved_org(self) -> None: + assert self._picked_org({}) == self._A + + def test_multiple_orgs_ignores_saved_org_no_longer_listed(self) -> None: + assert self._picked_org({"org_id": "44444444-4444-4444-4444-444444444444"}) == self._A + + def test_multiple_orgs_without_flag_or_tty_fails_without_prompting(self) -> None: + with ( + patch( + "sourcerykit.cli.init.service.get_organizations", + new=AsyncMock(return_value=[{"id": self._A}, {"id": self._B}]), + ), + patch("sourcerykit.cli.init.sys.stdin.isatty", return_value=False), + patch("sourcerykit.cli.init.questionary") as mock_q, + patch("sourcerykit.cli.init.save_app_dir_config") as mock_save, + patch("sourcerykit.cli.init.console"), + ): + assert _execute_post_auth_phases("at", email="u@example.com", sandbox=True) is False + + mock_q.select.assert_not_called() + mock_save.assert_not_called() + + class TestMaskSecret: def test_empty_string(self) -> None: assert mask_secret("") == "" From 528feac97ffb983400447cd2e2b06afd0eb68f69 Mon Sep 17 00:00:00 2001 From: SimoneBottoni Date: Fri, 25 Sep 2026 12:07:30 +0200 Subject: [PATCH 2/2] chore: bump version to 1.3.1 --- CHANGELOG.md | 5 +++++ pyproject.toml | 4 ++-- uv.lock | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 914c448..aa24535 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +## 1.3.1 + +### Features +- **`init --org-id-auto`** — when your account belongs to several organizations, `init` reuses the saved one (or the lowest ID) instead of prompting. Without the flag and without a terminal, `init` now exits with a clear error instead of crashing on the organization prompt. + ## 1.3.0 ### Breaking changes diff --git a/pyproject.toml b/pyproject.toml index cc506f6..e667348 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "sourcerykit" -version = "1.3.0" +version = "1.3.1" description = "Counterspell for hallucinating agents. Python SDK that breaks the illusion on every tool call, API response, and MCP handoff before bad outputs propagate." readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.12" @@ -154,7 +154,7 @@ exclude_also = ["if TYPE_CHECKING:", "raise NotImplementedError", "\\.\\.\\."] sourcerykit = "sourcerykit.cli.main:app" [tool.bumpversion] -current_version = "1.3.0" +current_version = "1.3.1" commit = false tag = false tag_name = "v{new_version}" diff --git a/uv.lock b/uv.lock index 240499e..853a1e7 100644 --- a/uv.lock +++ b/uv.lock @@ -2423,7 +2423,7 @@ wheels = [ [[package]] name = "sourcerykit" -version = "1.3.0" +version = "1.3.1" source = { editable = "." } dependencies = [ { name = "aiohttp" },