sovaria-cli 0.2.7__tar.gz → 0.2.9__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sovaria_cli-0.2.9/.github/dependabot.yml +12 -0
- sovaria_cli-0.2.9/.github/workflows/ci.yml +174 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/.github/workflows/publish.yml +5 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/.gitignore +4 -0
- sovaria_cli-0.2.9/.gitleaks.toml +17 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/PKG-INFO +42 -2
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/README.md +37 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/__init__.py +1 -1
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/commands/auth_product.py +17 -2
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/commands/db.py +84 -20
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/commands/deploy.py +75 -9
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/commands/governance.py +19 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/commands/login.py +279 -49
- sovaria_cli-0.2.9/apps/cli/src/commands/mcp.py +79 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/commands/org.py +5 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/commands/project.py +109 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/commands/realtime.py +37 -10
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/main.py +2 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/utils/api_client.py +109 -12
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/utils/auth_store.py +86 -19
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/utils/config.py +53 -0
- sovaria_cli-0.2.9/apps/cli/tests/test_aug18_cli_tickets.py +450 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/tests/test_cli_smoke_paths.py +26 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/tests/test_config_profiles.py +9 -0
- sovaria_cli-0.2.9/apps/cli/tests/test_credential_file_permissions.py +29 -0
- sovaria_cli-0.2.9/apps/cli/tests/test_db_migrations.py +62 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/tests/test_main_startup.py +1 -1
- sovaria_cli-0.2.9/apps/cli/tests/test_mcp_command.py +31 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/tests/test_supabase_style_commands.py +11 -1
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/pyproject.toml +19 -2
- sovaria_cli-0.2.9/requirements-dev.txt +4 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/requirements.txt +3 -1
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/scripts/cli_sdk_happy_path.sh +2 -2
- sovaria_cli-0.2.7/.github/workflows/ci.yml +0 -39
- sovaria_cli-0.2.7/requirements-dev.txt +0 -2
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/.python-version +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/Makefile +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/__init__.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/commands/__init__.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/commands/admin.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/commands/api.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/commands/audit.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/commands/billing.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/commands/chrono.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/commands/collections.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/commands/connect.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/commands/context.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/commands/dashboard.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/commands/data.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/commands/gen.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/commands/iam.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/commands/init_cmd.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/commands/inspect.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/commands/keys.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/commands/link_cmd.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/commands/logs.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/commands/oidc_providers.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/commands/passkeys.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/commands/policy.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/commands/schema.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/commands/scope.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/commands/secrets.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/commands/shell.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/commands/sim.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/commands/storage.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/commands/test_cmd.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/commands/vector.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/utils/__init__.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/utils/api_helpers.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/utils/display.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/utils/local_function_server.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/utils/log_tail.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/utils/migrations.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/utils/output_mode.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/utils/premium_display.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/utils/project_context.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/utils/schema_introspect.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/utils/scope_picker.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/src/utils/theme.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/tests/test_auth_store_profiles.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/tests/test_cli_security.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/tests/test_phase4_commands.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/tests/test_phase5_commands.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/tests/test_phase6_commands.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/tests/test_premium_display.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/tests/test_project_and_commands.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/apps/cli/tests/test_scope_picker.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/pytest.ini +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/scripts/smoke_all_commands.sh +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.9}/scripts/smoke_chunks.sh +0 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
workflow_call:
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
secret-scan:
|
|
12
|
+
name: Secret scan
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
with:
|
|
17
|
+
fetch-depth: 0
|
|
18
|
+
- name: Install Gitleaks
|
|
19
|
+
run: |
|
|
20
|
+
curl -sSfL https://github.com/gitleaks/gitleaks/releases/download/v8.24.2/gitleaks_8.24.2_linux_x64.tar.gz -o /tmp/gitleaks.tar.gz
|
|
21
|
+
tar -xzf /tmp/gitleaks.tar.gz -C /tmp
|
|
22
|
+
sudo install -m 0755 /tmp/gitleaks /usr/local/bin/gitleaks
|
|
23
|
+
- name: Run Gitleaks
|
|
24
|
+
run: |
|
|
25
|
+
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
|
26
|
+
RANGE="${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}"
|
|
27
|
+
else
|
|
28
|
+
RANGE="HEAD~1..HEAD"
|
|
29
|
+
fi
|
|
30
|
+
echo "Scanning commit range: $RANGE"
|
|
31
|
+
gitleaks detect --source . --redact --no-banner --config .gitleaks.toml --log-opts="$RANGE"
|
|
32
|
+
|
|
33
|
+
lint:
|
|
34
|
+
name: Ruff
|
|
35
|
+
runs-on: ubuntu-latest
|
|
36
|
+
needs: secret-scan
|
|
37
|
+
steps:
|
|
38
|
+
- uses: actions/checkout@v4
|
|
39
|
+
- name: Set up Python
|
|
40
|
+
uses: actions/setup-python@v5
|
|
41
|
+
with:
|
|
42
|
+
python-version: "3.12"
|
|
43
|
+
- name: Install ruff
|
|
44
|
+
run: pip install "ruff>=0.9"
|
|
45
|
+
- name: Ruff check
|
|
46
|
+
run: |
|
|
47
|
+
ruff check \
|
|
48
|
+
apps/cli/src/commands/login.py \
|
|
49
|
+
apps/cli/src/commands/deploy.py \
|
|
50
|
+
apps/cli/src/commands/realtime.py \
|
|
51
|
+
apps/cli/src/utils/api_client.py \
|
|
52
|
+
apps/cli/src/utils/auth_store.py \
|
|
53
|
+
apps/cli/src/utils/config.py \
|
|
54
|
+
apps/cli/tests/test_aug18_cli_tickets.py \
|
|
55
|
+
apps/cli/tests/test_config_profiles.py
|
|
56
|
+
|
|
57
|
+
typecheck:
|
|
58
|
+
name: Mypy
|
|
59
|
+
runs-on: ubuntu-latest
|
|
60
|
+
needs: secret-scan
|
|
61
|
+
env:
|
|
62
|
+
# Fine-grained token with contents:read on sovaria/sovaria-sdk. Without it, CI installs the
|
|
63
|
+
# pinned SDK release from PyPI instead.
|
|
64
|
+
SDK_TOKEN: ${{ secrets.SOVARIA_SDK_READ_TOKEN }}
|
|
65
|
+
steps:
|
|
66
|
+
- uses: actions/checkout@v4
|
|
67
|
+
- name: Check out the matching SDK (when SOVARIA_SDK_READ_TOKEN is set)
|
|
68
|
+
if: env.SDK_TOKEN != ''
|
|
69
|
+
uses: actions/checkout@v4
|
|
70
|
+
with:
|
|
71
|
+
repository: sovaria/sovaria-sdk
|
|
72
|
+
path: sovaria-sdk
|
|
73
|
+
token: ${{ env.SDK_TOKEN }}
|
|
74
|
+
- name: Set up Python
|
|
75
|
+
uses: actions/setup-python@v5
|
|
76
|
+
with:
|
|
77
|
+
python-version: "3.12"
|
|
78
|
+
- name: Install SDK + CLI
|
|
79
|
+
run: |
|
|
80
|
+
set -euo pipefail
|
|
81
|
+
if [ -f ../sovaria-sdk/apps/sdk/python/pyproject.toml ]; then
|
|
82
|
+
pip install -e ../sovaria-sdk/apps/sdk/python
|
|
83
|
+
elif [ -f sovaria-sdk/apps/sdk/python/pyproject.toml ]; then
|
|
84
|
+
pip install -e sovaria-sdk/apps/sdk/python
|
|
85
|
+
else
|
|
86
|
+
pip install "sovaria-sdk==0.4.0"
|
|
87
|
+
fi
|
|
88
|
+
pip install -e ".[dev]"
|
|
89
|
+
- name: Mypy
|
|
90
|
+
run: |
|
|
91
|
+
mypy \
|
|
92
|
+
apps/cli/src/commands/login.py \
|
|
93
|
+
apps/cli/src/commands/deploy.py \
|
|
94
|
+
apps/cli/src/commands/realtime.py \
|
|
95
|
+
apps/cli/src/utils/api_client.py \
|
|
96
|
+
apps/cli/src/utils/auth_store.py \
|
|
97
|
+
apps/cli/src/utils/config.py
|
|
98
|
+
|
|
99
|
+
install-requirements:
|
|
100
|
+
name: requirements.txt install
|
|
101
|
+
runs-on: ubuntu-latest
|
|
102
|
+
needs: secret-scan
|
|
103
|
+
env:
|
|
104
|
+
# Fine-grained token with contents:read on sovaria/sovaria-sdk. Without it, CI installs the
|
|
105
|
+
# pinned SDK release from PyPI instead.
|
|
106
|
+
SDK_TOKEN: ${{ secrets.SOVARIA_SDK_READ_TOKEN }}
|
|
107
|
+
steps:
|
|
108
|
+
- uses: actions/checkout@v4
|
|
109
|
+
- name: Check out the matching SDK (when SOVARIA_SDK_READ_TOKEN is set)
|
|
110
|
+
if: env.SDK_TOKEN != ''
|
|
111
|
+
uses: actions/checkout@v4
|
|
112
|
+
with:
|
|
113
|
+
repository: sovaria/sovaria-sdk
|
|
114
|
+
path: sovaria-sdk
|
|
115
|
+
token: ${{ env.SDK_TOKEN }}
|
|
116
|
+
- name: Set up Python
|
|
117
|
+
uses: actions/setup-python@v5
|
|
118
|
+
with:
|
|
119
|
+
python-version: "3.12"
|
|
120
|
+
- name: Install from requirements.txt
|
|
121
|
+
run: |
|
|
122
|
+
set -euo pipefail
|
|
123
|
+
if [ -f ../sovaria-sdk/apps/sdk/python/pyproject.toml ]; then
|
|
124
|
+
pip install -e ../sovaria-sdk/apps/sdk/python
|
|
125
|
+
elif [ -f sovaria-sdk/apps/sdk/python/pyproject.toml ]; then
|
|
126
|
+
pip install -e sovaria-sdk/apps/sdk/python
|
|
127
|
+
else
|
|
128
|
+
pip install "sovaria-sdk==0.4.0"
|
|
129
|
+
fi
|
|
130
|
+
pip install -r requirements.txt
|
|
131
|
+
pip install --no-deps .
|
|
132
|
+
- name: sov --help
|
|
133
|
+
run: sov --help
|
|
134
|
+
|
|
135
|
+
test:
|
|
136
|
+
runs-on: ubuntu-latest
|
|
137
|
+
needs: secret-scan
|
|
138
|
+
env:
|
|
139
|
+
SDK_TOKEN: ${{ secrets.SOVARIA_SDK_READ_TOKEN }}
|
|
140
|
+
steps:
|
|
141
|
+
- uses: actions/checkout@v4
|
|
142
|
+
- name: Check out the matching SDK (when SOVARIA_SDK_READ_TOKEN is set)
|
|
143
|
+
if: env.SDK_TOKEN != ''
|
|
144
|
+
uses: actions/checkout@v4
|
|
145
|
+
with:
|
|
146
|
+
repository: sovaria/sovaria-sdk
|
|
147
|
+
path: sovaria-sdk
|
|
148
|
+
token: ${{ env.SDK_TOKEN }}
|
|
149
|
+
|
|
150
|
+
- name: Set up Python
|
|
151
|
+
uses: actions/setup-python@v5
|
|
152
|
+
with:
|
|
153
|
+
python-version: "3.12"
|
|
154
|
+
|
|
155
|
+
- name: Install SDK + CLI
|
|
156
|
+
run: |
|
|
157
|
+
set -euo pipefail
|
|
158
|
+
if [ -f ../sovaria-sdk/apps/sdk/python/pyproject.toml ]; then
|
|
159
|
+
pip install -e ../sovaria-sdk/apps/sdk/python
|
|
160
|
+
elif [ -f sovaria-sdk/apps/sdk/python/pyproject.toml ]; then
|
|
161
|
+
pip install -e sovaria-sdk/apps/sdk/python
|
|
162
|
+
else
|
|
163
|
+
pip install "sovaria-sdk==0.4.0"
|
|
164
|
+
fi
|
|
165
|
+
pip install -e ".[dev]"
|
|
166
|
+
|
|
167
|
+
- name: Run tests
|
|
168
|
+
run: pytest apps/cli/tests -q
|
|
169
|
+
|
|
170
|
+
- name: Build wheel
|
|
171
|
+
run: python -m build
|
|
172
|
+
|
|
173
|
+
- name: Validate wheel metadata
|
|
174
|
+
run: python -m twine check dist/*
|
|
@@ -8,8 +8,13 @@ on:
|
|
|
8
8
|
required: true
|
|
9
9
|
|
|
10
10
|
jobs:
|
|
11
|
+
test:
|
|
12
|
+
if: github.event.inputs.confirm == 'publish'
|
|
13
|
+
uses: ./.github/workflows/ci.yml
|
|
14
|
+
|
|
11
15
|
publish:
|
|
12
16
|
if: github.event.inputs.confirm == 'publish'
|
|
17
|
+
needs: test
|
|
13
18
|
runs-on: ubuntu-latest
|
|
14
19
|
environment: pypi
|
|
15
20
|
permissions:
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
title = "Sovaria CLI gitleaks config"
|
|
2
|
+
|
|
3
|
+
[extend]
|
|
4
|
+
useDefault = true
|
|
5
|
+
|
|
6
|
+
[allowlist]
|
|
7
|
+
description = "Placeholder credentials and CLI test fixtures"
|
|
8
|
+
paths = [
|
|
9
|
+
'''\.env\.example$''',
|
|
10
|
+
'''apps/cli/tests/''',
|
|
11
|
+
]
|
|
12
|
+
regexes = [
|
|
13
|
+
'''change-me''',
|
|
14
|
+
'''CHANGE_ME''',
|
|
15
|
+
'''do-not-use-in-prod''',
|
|
16
|
+
'''replace-with''',
|
|
17
|
+
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sovaria-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.9
|
|
4
4
|
Summary: Sovaria BaaS command-line interface (sov)
|
|
5
5
|
Project-URL: Homepage, https://sovaria.in
|
|
6
6
|
Project-URL: Documentation, https://docs.sovaria.in/cli
|
|
@@ -12,14 +12,17 @@ Requires-Dist: httpx>=0.28.1
|
|
|
12
12
|
Requires-Dist: keyring>=25.0
|
|
13
13
|
Requires-Dist: pyyaml>=6.0
|
|
14
14
|
Requires-Dist: rich>=13.7.0
|
|
15
|
-
Requires-Dist: sovaria-sdk
|
|
15
|
+
Requires-Dist: sovaria-sdk==0.4.1
|
|
16
16
|
Requires-Dist: tomli-w>=1.2.0
|
|
17
17
|
Requires-Dist: typer>=0.17.4
|
|
18
18
|
Provides-Extra: dev
|
|
19
19
|
Requires-Dist: build>=1.2; extra == 'dev'
|
|
20
|
+
Requires-Dist: mypy>=1.13; extra == 'dev'
|
|
20
21
|
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
21
22
|
Requires-Dist: ruff>=0.9; extra == 'dev'
|
|
22
23
|
Requires-Dist: twine>=6.0; extra == 'dev'
|
|
24
|
+
Provides-Extra: mcp
|
|
25
|
+
Requires-Dist: sovaria-sdk[mcp]==0.4.1; extra == 'mcp'
|
|
23
26
|
Description-Content-Type: text/markdown
|
|
24
27
|
|
|
25
28
|
# Sovaria CLI
|
|
@@ -61,6 +64,43 @@ scripts/cli_sdk_happy_path.sh
|
|
|
61
64
|
|
|
62
65
|
If you already have valid CLI tokens, omit `SOVARIA_EMAIL` and `SOVARIA_PASSWORD`.
|
|
63
66
|
|
|
67
|
+
## Authentication
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
sov login
|
|
71
|
+
sov login --email you@example.com
|
|
72
|
+
sov login --file-storage # headless: tokens in ~/.sovaria/credentials.toml
|
|
73
|
+
sov logout
|
|
74
|
+
sov logout --force # clear local tokens if server revoke fails
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
- **Browser (PKCE):** authorize opens the console origin (`get_console_url`); the token exchange POSTs to the gateway `api_url` `/oauth/token` proxy. That split is intentional.
|
|
78
|
+
- **Password:** never pass `--password`. Operator login prompts with `getpass`, or reads `SOVARIA_PASSWORD`.
|
|
79
|
+
- **Product user create:** `sov auth-product create --email ...` prompts for the new user's password, or reads `SOVARIA_USER_PASSWORD`. Do not reuse `SOVARIA_PASSWORD` (that is the operator login secret).
|
|
80
|
+
- **Magic link:** `sov auth magic-link verify` reads the token from stdin or `SOVARIA_MAGIC_TOKEN`.
|
|
81
|
+
- **GitHub deploy PAT:** set `SOVARIA_GITHUB_TOKEN` or use `gh auth token`. There is no `--token` flag.
|
|
82
|
+
- **OAuth callback:** binds `127.0.0.1` by default. Non-loopback `SOVARIA_CLI_CALLBACK_HOST` requires `--insecure-callback`.
|
|
83
|
+
|
|
84
|
+
### Token refresh contract
|
|
85
|
+
|
|
86
|
+
Axis supports two refresh modes. Do not mix them:
|
|
87
|
+
|
|
88
|
+
| Client | How refresh is sent |
|
|
89
|
+
|---|---|
|
|
90
|
+
| **CLI / Python SDK** | JSON body: `POST /api/v1/auth/refresh` with `{"refresh_token": "<stored>"}`. The CLI keeps the refresh token in OS keyring (or `credentials.toml` with `--file-storage`). |
|
|
91
|
+
| **Console / browser** | Empty JSON body `{}` plus the HttpOnly refresh cookie. The browser never puts the refresh token in the request body. |
|
|
92
|
+
|
|
93
|
+
Copying the console cookie pattern into a CLI or SDK client will fail silently (no cookie jar). Copying the CLI JSON-body pattern into a browser app bypasses HttpOnly cookie storage.
|
|
94
|
+
|
|
95
|
+
### Pulse WebSocket URLs
|
|
96
|
+
|
|
97
|
+
`sov realtime ws-url` issues an opaque `ticket=` query parameter. Never put a JWT in `?token=` — production gateways disable `GATEWAY_WS_ALLOW_QUERY_TOKEN`. Local `.env.local` may still enable the legacy query-token path; do not copy that into clients.
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
sov realtime ws-url my-channel --org <org_id> --project <project_id>
|
|
101
|
+
# wss://api.sovaria.in/api/v1/rt/ws/my-channel?ticket=...&org_id=...&project_id=...
|
|
102
|
+
```
|
|
103
|
+
|
|
64
104
|
## Core Command Groups
|
|
65
105
|
|
|
66
106
|
- `sov auth ...` auth login/logout
|
|
@@ -37,6 +37,43 @@ scripts/cli_sdk_happy_path.sh
|
|
|
37
37
|
|
|
38
38
|
If you already have valid CLI tokens, omit `SOVARIA_EMAIL` and `SOVARIA_PASSWORD`.
|
|
39
39
|
|
|
40
|
+
## Authentication
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
sov login
|
|
44
|
+
sov login --email you@example.com
|
|
45
|
+
sov login --file-storage # headless: tokens in ~/.sovaria/credentials.toml
|
|
46
|
+
sov logout
|
|
47
|
+
sov logout --force # clear local tokens if server revoke fails
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
- **Browser (PKCE):** authorize opens the console origin (`get_console_url`); the token exchange POSTs to the gateway `api_url` `/oauth/token` proxy. That split is intentional.
|
|
51
|
+
- **Password:** never pass `--password`. Operator login prompts with `getpass`, or reads `SOVARIA_PASSWORD`.
|
|
52
|
+
- **Product user create:** `sov auth-product create --email ...` prompts for the new user's password, or reads `SOVARIA_USER_PASSWORD`. Do not reuse `SOVARIA_PASSWORD` (that is the operator login secret).
|
|
53
|
+
- **Magic link:** `sov auth magic-link verify` reads the token from stdin or `SOVARIA_MAGIC_TOKEN`.
|
|
54
|
+
- **GitHub deploy PAT:** set `SOVARIA_GITHUB_TOKEN` or use `gh auth token`. There is no `--token` flag.
|
|
55
|
+
- **OAuth callback:** binds `127.0.0.1` by default. Non-loopback `SOVARIA_CLI_CALLBACK_HOST` requires `--insecure-callback`.
|
|
56
|
+
|
|
57
|
+
### Token refresh contract
|
|
58
|
+
|
|
59
|
+
Axis supports two refresh modes. Do not mix them:
|
|
60
|
+
|
|
61
|
+
| Client | How refresh is sent |
|
|
62
|
+
|---|---|
|
|
63
|
+
| **CLI / Python SDK** | JSON body: `POST /api/v1/auth/refresh` with `{"refresh_token": "<stored>"}`. The CLI keeps the refresh token in OS keyring (or `credentials.toml` with `--file-storage`). |
|
|
64
|
+
| **Console / browser** | Empty JSON body `{}` plus the HttpOnly refresh cookie. The browser never puts the refresh token in the request body. |
|
|
65
|
+
|
|
66
|
+
Copying the console cookie pattern into a CLI or SDK client will fail silently (no cookie jar). Copying the CLI JSON-body pattern into a browser app bypasses HttpOnly cookie storage.
|
|
67
|
+
|
|
68
|
+
### Pulse WebSocket URLs
|
|
69
|
+
|
|
70
|
+
`sov realtime ws-url` issues an opaque `ticket=` query parameter. Never put a JWT in `?token=` — production gateways disable `GATEWAY_WS_ALLOW_QUERY_TOKEN`. Local `.env.local` may still enable the legacy query-token path; do not copy that into clients.
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
sov realtime ws-url my-channel --org <org_id> --project <project_id>
|
|
74
|
+
# wss://api.sovaria.in/api/v1/rt/ws/my-channel?ticket=...&org_id=...&project_id=...
|
|
75
|
+
```
|
|
76
|
+
|
|
40
77
|
## Core Command Groups
|
|
41
78
|
|
|
42
79
|
- `sov auth ...` auth login/logout
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import getpass
|
|
6
|
+
import os
|
|
5
7
|
from typing import TypedDict
|
|
6
8
|
from urllib.parse import quote
|
|
7
9
|
|
|
@@ -9,7 +11,7 @@ import typer
|
|
|
9
11
|
|
|
10
12
|
from apps.cli.src.utils.api_client import CLIRequestError, call_api
|
|
11
13
|
from apps.cli.src.utils.display import handle_cli_error, print_json
|
|
12
|
-
from apps.cli.src.utils.premium_display import run_with_spinner
|
|
14
|
+
from apps.cli.src.utils.premium_display import print_error_panel, run_with_spinner
|
|
13
15
|
from apps.cli.src.utils.config import get_scope
|
|
14
16
|
|
|
15
17
|
app = typer.Typer(help="Manage product users within an org/project scope")
|
|
@@ -33,6 +35,19 @@ def _scope_args(
|
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
|
|
38
|
+
def _read_new_user_password() -> str:
|
|
39
|
+
env = (os.getenv("SOVARIA_USER_PASSWORD") or "").strip()
|
|
40
|
+
if env:
|
|
41
|
+
return env
|
|
42
|
+
password = getpass.getpass("New user password: ")
|
|
43
|
+
if not password:
|
|
44
|
+
print_error_panel(
|
|
45
|
+
"Password is required. Enter it at the prompt or set SOVARIA_USER_PASSWORD."
|
|
46
|
+
)
|
|
47
|
+
raise typer.Exit(1)
|
|
48
|
+
return password
|
|
49
|
+
|
|
50
|
+
|
|
36
51
|
def _project_auth_path(profile: str | None, project_id: str | None, suffix: str) -> str:
|
|
37
52
|
resolved_project = (project_id or get_scope(profile).get("project_id") or "").strip()
|
|
38
53
|
if not resolved_project:
|
|
@@ -164,7 +179,6 @@ def set_roles(
|
|
|
164
179
|
@app.command("create")
|
|
165
180
|
def create_user(
|
|
166
181
|
email: str = typer.Option(..., "--email"),
|
|
167
|
-
password: str = typer.Option(..., "--password"),
|
|
168
182
|
name: str | None = typer.Option(None, "--name", help="Display name (defaults to email local-part)"),
|
|
169
183
|
profile: str | None = typer.Option(None, "--profile"),
|
|
170
184
|
org_id: str | None = typer.Option(None, "--org"),
|
|
@@ -173,6 +187,7 @@ def create_user(
|
|
|
173
187
|
) -> None:
|
|
174
188
|
"""Create a product user in the current project scope."""
|
|
175
189
|
display_name = name or email.split("@", maxsplit=1)[0]
|
|
190
|
+
password = _read_new_user_password()
|
|
176
191
|
try:
|
|
177
192
|
result = run_with_spinner(
|
|
178
193
|
f"Creating user {email}…",
|
|
@@ -612,6 +612,34 @@ def migrations_list(
|
|
|
612
612
|
print_json(result)
|
|
613
613
|
|
|
614
614
|
|
|
615
|
+
def _split_migration(path: Path) -> tuple[str, str | None]:
|
|
616
|
+
"""Up and optional down SQL: a ``-- migrate:down`` section in the file, or a sibling ``<name>.down.sql``."""
|
|
617
|
+
import re as _re
|
|
618
|
+
|
|
619
|
+
text = path.read_text(encoding="utf-8")
|
|
620
|
+
parts = _re.split(r"^\s*--\s*migrate:down\s*$", text, maxsplit=1, flags=_re.MULTILINE | _re.IGNORECASE)
|
|
621
|
+
up = _re.sub(r"^\s*--\s*migrate:up\s*$", "", parts[0], flags=_re.MULTILINE | _re.IGNORECASE).strip()
|
|
622
|
+
down = parts[1].strip() if len(parts) > 1 else None
|
|
623
|
+
sibling = path.with_name(f"{path.stem}.down.sql")
|
|
624
|
+
if not down and sibling.exists():
|
|
625
|
+
down = sibling.read_text(encoding="utf-8").strip() or None
|
|
626
|
+
return up, down
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def _remote_migration_names(scope: dict[str, str | None]) -> set[str]:
|
|
630
|
+
"""Names of migrations currently applied on the server (pages through the whole history)."""
|
|
631
|
+
names: set[str] = set()
|
|
632
|
+
offset, page = 0, 500 # server maximum
|
|
633
|
+
while True:
|
|
634
|
+
result = call_api(method="GET", path="/api/v1/data/rel/migrations",
|
|
635
|
+
params={"limit": str(page), "offset": str(offset)}, require_scope=True, **scope)
|
|
636
|
+
rows = result.get("migrations", []) if isinstance(result, dict) else []
|
|
637
|
+
names |= {str(r.get("name")) for r in rows if isinstance(r, dict) and r.get("status", "applied") == "applied"}
|
|
638
|
+
if len(rows) < page:
|
|
639
|
+
return names
|
|
640
|
+
offset += page
|
|
641
|
+
|
|
642
|
+
|
|
615
643
|
@migrations_app.command("push")
|
|
616
644
|
def migrations_push(
|
|
617
645
|
directory: str | None = typer.Option(
|
|
@@ -619,60 +647,96 @@ def migrations_push(
|
|
|
619
647
|
"--dir",
|
|
620
648
|
help="Migration directory (default: sovaria/migrations under linked project)",
|
|
621
649
|
),
|
|
622
|
-
dry_run: bool = typer.Option(
|
|
650
|
+
dry_run: bool = typer.Option(
|
|
651
|
+
False, "--dry-run", help="Run each pending migration in a transaction and roll it back (nothing is kept)"
|
|
652
|
+
),
|
|
623
653
|
profile: str | None = typer.Option(None, "--profile"),
|
|
624
654
|
org_id: str | None = typer.Option(None, "--org"),
|
|
625
655
|
project_id: str | None = typer.Option(None, "--project"),
|
|
626
656
|
env_id: str | None = typer.Option(None, "--env"),
|
|
627
657
|
) -> None:
|
|
628
|
-
"""Apply pending
|
|
658
|
+
"""Apply pending .sql migrations through the project's migration history.
|
|
659
|
+
|
|
660
|
+
Each file is applied atomically and recorded on the server, so every teammate and CI run sees the
|
|
661
|
+
same history. Put rollback SQL under a ``-- migrate:down`` line (or in ``<name>.down.sql``) to make
|
|
662
|
+
a migration reversible with ``sov db migrations rollback``.
|
|
663
|
+
"""
|
|
629
664
|
from apps.cli.src.utils.migrations import (
|
|
630
665
|
list_local_migrations,
|
|
631
666
|
load_applied_state,
|
|
632
|
-
migration_name,
|
|
633
667
|
migrations_dir,
|
|
634
668
|
save_applied_state,
|
|
635
669
|
)
|
|
636
670
|
from apps.cli.src.utils.premium_display import _console
|
|
637
671
|
|
|
638
672
|
folder = Path(directory) if directory else migrations_dir()
|
|
639
|
-
files = list_local_migrations(folder)
|
|
673
|
+
files = [p for p in list_local_migrations(folder) if not p.name.endswith(".down.sql")]
|
|
640
674
|
if not files:
|
|
641
675
|
print_warning(f"No .sql files in {folder}. Create migrations there first.")
|
|
642
676
|
raise typer.Exit(0)
|
|
643
677
|
|
|
644
|
-
|
|
645
|
-
|
|
678
|
+
scope = _scope_opts(profile, org_id, project_id, env_id)
|
|
679
|
+
try:
|
|
680
|
+
remote = run_with_spinner("Reading migration history…", lambda: _remote_migration_names(scope))
|
|
681
|
+
except CLIRequestError as exc:
|
|
682
|
+
handle_cli_error(exc)
|
|
683
|
+
pending = [path for path in files if path.stem not in remote]
|
|
646
684
|
if not pending:
|
|
647
|
-
print_success("
|
|
685
|
+
print_success("Database is up to date — every local migration is in the project history.")
|
|
648
686
|
raise typer.Exit(0)
|
|
649
687
|
|
|
650
688
|
_console.print(f"\n[bold]Pending migrations ({len(pending)}):[/bold]")
|
|
651
689
|
for path in pending:
|
|
652
690
|
_console.print(f" • {path.name}")
|
|
653
691
|
|
|
654
|
-
if dry_run
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
scope = _scope_opts(profile, org_id, project_id, env_id)
|
|
692
|
+
endpoint = "/api/v1/data/rel/migrations/dry-run" if dry_run else "/api/v1/data/rel/migrations/apply"
|
|
693
|
+
applied_local = load_applied_state()
|
|
658
694
|
for path in pending:
|
|
659
|
-
|
|
660
|
-
if not
|
|
695
|
+
up, down = _split_migration(path)
|
|
696
|
+
if not up:
|
|
661
697
|
print_warning(f"Skipping empty file: {path.name}")
|
|
662
|
-
applied.add(migration_name(path))
|
|
663
698
|
continue
|
|
699
|
+
body = {"name": path.stem[:120], "up_sql": up, **({"down_sql": down} if down else {})}
|
|
664
700
|
try:
|
|
665
701
|
run_with_spinner(
|
|
666
|
-
f"Applying {path.name}…",
|
|
667
|
-
lambda
|
|
668
|
-
success=f"Applied {path.name}",
|
|
702
|
+
f"{'Checking' if dry_run else 'Applying'} {path.name}…",
|
|
703
|
+
lambda b=body: call_api(method="POST", path=endpoint, body=b, require_scope=True, **scope),
|
|
704
|
+
success=f"{'Would apply' if dry_run else 'Applied'} {path.name}",
|
|
669
705
|
)
|
|
670
706
|
except CLIRequestError as exc:
|
|
671
707
|
handle_cli_error(exc)
|
|
672
|
-
|
|
708
|
+
if not dry_run:
|
|
709
|
+
applied_local.add(path.name)
|
|
710
|
+
|
|
711
|
+
if dry_run:
|
|
712
|
+
print_success(f"All {len(pending)} pending migration(s) apply cleanly. Nothing was changed.")
|
|
713
|
+
return
|
|
714
|
+
save_applied_state(applied_local)
|
|
715
|
+
print_success(f"Applied {len(pending)} migration(s).")
|
|
673
716
|
|
|
674
|
-
|
|
675
|
-
|
|
717
|
+
|
|
718
|
+
@migrations_app.command("rollback")
|
|
719
|
+
def migrations_rollback(
|
|
720
|
+
version: str | None = typer.Option(None, "--version", help="Roll back this version (needs --force if it isn't the latest)"),
|
|
721
|
+
force: bool = typer.Option(False, "--force", help="Allow rolling back a migration that isn't the most recent"),
|
|
722
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
723
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
724
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
725
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
726
|
+
) -> None:
|
|
727
|
+
"""Undo the most recent migration using its down SQL."""
|
|
728
|
+
scope = _scope_opts(profile, org_id, project_id, env_id)
|
|
729
|
+
params = {**({"version": version} if version else {}), **({"force": "true"} if force else {})}
|
|
730
|
+
try:
|
|
731
|
+
result = run_with_spinner(
|
|
732
|
+
"Rolling back…",
|
|
733
|
+
lambda: call_api(method="POST", path="/api/v1/data/rel/migrations/rollback", params=params,
|
|
734
|
+
require_scope=True, **scope),
|
|
735
|
+
success="Rolled back",
|
|
736
|
+
)
|
|
737
|
+
except CLIRequestError as exc:
|
|
738
|
+
handle_cli_error(exc)
|
|
739
|
+
print_json(result)
|
|
676
740
|
|
|
677
741
|
|
|
678
742
|
@migrations_app.command("pull")
|