Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion docs/getting_started/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:**
Expand All @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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}"
Expand Down
21 changes: 18 additions & 3 deletions src/sourcerykit/cli/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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()
Expand Down Expand Up @@ -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."""

Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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()
Expand All @@ -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)
5 changes: 4 additions & 1 deletion src/sourcerykit/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
48 changes: 48 additions & 0 deletions tests/unit/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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:
Expand All @@ -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("") == ""
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading