gravi-cli 0.1.1__py3-none-any.whl
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.
- gravi_cli/__init__.py +9 -0
- gravi_cli/api.py +49 -0
- gravi_cli/auth.py +139 -0
- gravi_cli/cli.py +401 -0
- gravi_cli/client.py +286 -0
- gravi_cli/config.py +134 -0
- gravi_cli/exceptions.py +36 -0
- gravi_cli-0.1.1.dist-info/METADATA +345 -0
- gravi_cli-0.1.1.dist-info/RECORD +12 -0
- gravi_cli-0.1.1.dist-info/WHEEL +4 -0
- gravi_cli-0.1.1.dist-info/entry_points.txt +2 -0
- gravi_cli-0.1.1.dist-info/licenses/LICENSE +31 -0
gravi_cli/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Gravi CLI - Command-line tool for Gravitate infrastructure management.
|
|
3
|
+
|
|
4
|
+
This package provides CLI access to Gravitate's mom infrastructure,
|
|
5
|
+
allowing developers to authenticate, manage tokens, and access instance
|
|
6
|
+
configurations and credentials.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
__version__ = "0.1.1"
|
gravi_cli/api.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Public Python API for gravi_cli.
|
|
3
|
+
|
|
4
|
+
This module provides the programmatic interface for using gravi_cli
|
|
5
|
+
as a library in Python scripts and applications.
|
|
6
|
+
|
|
7
|
+
Example usage:
|
|
8
|
+
from gravi_cli.api import get_instance_config, get_instance_token
|
|
9
|
+
|
|
10
|
+
# Get database credentials
|
|
11
|
+
config = get_instance_config("prod")
|
|
12
|
+
db_url = config["config"]["database_url"]
|
|
13
|
+
|
|
14
|
+
# Get ServiceNow access token
|
|
15
|
+
token_response = get_instance_token("dev")
|
|
16
|
+
sn_token = token_response["access_token"]
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
# Re-export the main API functions from auth module
|
|
20
|
+
from .auth import (
|
|
21
|
+
get_mom_token,
|
|
22
|
+
get_instance_config,
|
|
23
|
+
get_instance_token,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# Re-export exceptions for library users
|
|
27
|
+
from .exceptions import (
|
|
28
|
+
GraviError,
|
|
29
|
+
NotAuthenticatedError,
|
|
30
|
+
InvalidTokenError,
|
|
31
|
+
APIError,
|
|
32
|
+
RateLimitError,
|
|
33
|
+
ConfigError,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Public API
|
|
37
|
+
__all__ = [
|
|
38
|
+
# Authentication functions
|
|
39
|
+
"get_mom_token",
|
|
40
|
+
"get_instance_config",
|
|
41
|
+
"get_instance_token",
|
|
42
|
+
# Exceptions
|
|
43
|
+
"GraviError",
|
|
44
|
+
"NotAuthenticatedError",
|
|
45
|
+
"InvalidTokenError",
|
|
46
|
+
"APIError",
|
|
47
|
+
"RateLimitError",
|
|
48
|
+
"ConfigError",
|
|
49
|
+
]
|
gravi_cli/auth.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Authentication and token management for gravi CLI.
|
|
3
|
+
|
|
4
|
+
Handles token refresh, auto-renewal, and provides the main API
|
|
5
|
+
for accessing mom and instance credentials.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
from datetime import datetime, timedelta, UTC
|
|
10
|
+
|
|
11
|
+
from .client import MomClient
|
|
12
|
+
from .config import load_config, save_config, config_exists
|
|
13
|
+
from .exceptions import NotAuthenticatedError, InvalidTokenError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_mom_url() -> str:
|
|
17
|
+
"""
|
|
18
|
+
Get mom URL from environment or use default.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
Mom API base URL
|
|
22
|
+
|
|
23
|
+
Priority:
|
|
24
|
+
1. GRAVI_MOM_URL environment variable
|
|
25
|
+
2. Default: https://mom.gravitate.energy/api
|
|
26
|
+
"""
|
|
27
|
+
return os.getenv("GRAVI_MOM_URL", "https://mom.gravitate.energy/api")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_mom_token() -> str:
|
|
31
|
+
"""
|
|
32
|
+
Get valid Mom access token, refreshing if necessary.
|
|
33
|
+
|
|
34
|
+
This function:
|
|
35
|
+
1. Checks for GRAVI_REFRESH_TOKEN environment variable (CI/CD usage)
|
|
36
|
+
2. Falls back to config file refresh token
|
|
37
|
+
3. Refreshes the access token using the refresh token
|
|
38
|
+
4. Auto-renews refresh token if <7 days remaining (saves to config)
|
|
39
|
+
5. Returns fresh access token (in-memory only, not persisted)
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Valid mom access token
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
NotAuthenticatedError: If not logged in or token expired
|
|
46
|
+
InvalidTokenError: If refresh token is invalid or revoked
|
|
47
|
+
"""
|
|
48
|
+
# Check for CI/CD environment variable first
|
|
49
|
+
refresh_token = os.getenv("GRAVI_REFRESH_TOKEN")
|
|
50
|
+
|
|
51
|
+
# Fall back to config file if not in CI/CD
|
|
52
|
+
if not refresh_token:
|
|
53
|
+
if not config_exists():
|
|
54
|
+
raise NotAuthenticatedError("Please run 'gravi login' first")
|
|
55
|
+
|
|
56
|
+
config = load_config()
|
|
57
|
+
refresh_token = config.refresh_token
|
|
58
|
+
else:
|
|
59
|
+
# CI/CD mode - load config if it exists (for potential auto-renewal)
|
|
60
|
+
config = None
|
|
61
|
+
if config_exists():
|
|
62
|
+
config = load_config()
|
|
63
|
+
|
|
64
|
+
if not refresh_token:
|
|
65
|
+
raise NotAuthenticatedError("Please run 'gravi login' first")
|
|
66
|
+
|
|
67
|
+
# Always get fresh access token from refresh token
|
|
68
|
+
mom_url = get_mom_url()
|
|
69
|
+
client = MomClient(mom_url)
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
response = client.refresh_token(refresh_token)
|
|
73
|
+
except InvalidTokenError:
|
|
74
|
+
raise NotAuthenticatedError("Refresh token expired or revoked. Please run 'gravi login' again")
|
|
75
|
+
|
|
76
|
+
# Check if refresh token was renewed (auto-renewal if <7 days remaining)
|
|
77
|
+
if "refresh_token" in response and config:
|
|
78
|
+
config.refresh_token = response["refresh_token"]
|
|
79
|
+
config.refresh_token_expires_at = datetime.now(UTC) + timedelta(seconds=response["refresh_expires_in"])
|
|
80
|
+
save_config(config)
|
|
81
|
+
|
|
82
|
+
# Return access token (in-memory only, not persisted)
|
|
83
|
+
return response["access_token"]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def get_instance_config(instance_key: str) -> dict:
|
|
87
|
+
"""
|
|
88
|
+
Get instance configuration (credentials, URLs, etc.).
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
instance_key: Instance identifier (e.g., "dev", "prod")
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Instance configuration dictionary:
|
|
95
|
+
{
|
|
96
|
+
"instance_key": "dev",
|
|
97
|
+
"name": "Development Environment",
|
|
98
|
+
"api_url": "https://dev-api.gravitate.com",
|
|
99
|
+
"config": {
|
|
100
|
+
"database_url": "postgresql://...",
|
|
101
|
+
"redis_url": "redis://...",
|
|
102
|
+
...
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
Raises:
|
|
107
|
+
NotAuthenticatedError: If not logged in or token expired
|
|
108
|
+
InvalidTokenError: If token is invalid or revoked
|
|
109
|
+
APIError: If API request fails
|
|
110
|
+
"""
|
|
111
|
+
mom_url = get_mom_url()
|
|
112
|
+
mom_token = get_mom_token()
|
|
113
|
+
client = MomClient(mom_url)
|
|
114
|
+
return client.get_instance_config(instance_key, mom_token)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def get_instance_token(instance_key: str) -> dict:
|
|
118
|
+
"""
|
|
119
|
+
Get instance access token/credentials.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
instance_key: Instance identifier (e.g., "dev", "prod")
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Instance-specific credentials (format varies by instance type)
|
|
126
|
+
Examples:
|
|
127
|
+
- ServiceNow OAuth: {"access_token": "...", "token_type": "Bearer", "expires_in": 3600}
|
|
128
|
+
- API Key based: {"api_key": "...", "environment": "production"}
|
|
129
|
+
- Custom format: Any JSON structure the instance provides
|
|
130
|
+
|
|
131
|
+
Raises:
|
|
132
|
+
NotAuthenticatedError: If not logged in or token expired
|
|
133
|
+
InvalidTokenError: If token is invalid or revoked
|
|
134
|
+
APIError: If API request fails
|
|
135
|
+
"""
|
|
136
|
+
mom_url = get_mom_url()
|
|
137
|
+
mom_token = get_mom_token()
|
|
138
|
+
client = MomClient(mom_url)
|
|
139
|
+
return client.get_instance_token(instance_key, mom_token)
|
gravi_cli/cli.py
ADDED
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main CLI interface for gravi.
|
|
3
|
+
|
|
4
|
+
Implements all CLI commands using Click framework.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
import json
|
|
10
|
+
import time
|
|
11
|
+
import socket
|
|
12
|
+
import shlex
|
|
13
|
+
import webbrowser
|
|
14
|
+
from datetime import datetime, UTC
|
|
15
|
+
|
|
16
|
+
import click
|
|
17
|
+
|
|
18
|
+
from . import __version__
|
|
19
|
+
from .auth import get_mom_url, get_mom_token, get_instance_config, get_instance_token
|
|
20
|
+
from .client import MomClient
|
|
21
|
+
from .config import (
|
|
22
|
+
Config,
|
|
23
|
+
load_config,
|
|
24
|
+
save_config,
|
|
25
|
+
delete_config,
|
|
26
|
+
config_exists,
|
|
27
|
+
get_config_path,
|
|
28
|
+
)
|
|
29
|
+
from .exceptions import (
|
|
30
|
+
NotAuthenticatedError,
|
|
31
|
+
InvalidTokenError,
|
|
32
|
+
APIError,
|
|
33
|
+
RateLimitError,
|
|
34
|
+
ConfigError,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@click.group()
|
|
39
|
+
@click.version_option(version=__version__, prog_name="gravi")
|
|
40
|
+
def main():
|
|
41
|
+
"""Gravi CLI - Gravitate infrastructure management tool."""
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@main.command()
|
|
46
|
+
@click.option(
|
|
47
|
+
"--mom-url",
|
|
48
|
+
envvar="GRAVI_MOM_URL",
|
|
49
|
+
default="https://mom.gravitate.energy/api",
|
|
50
|
+
help="Mom API URL (default: https://mom.gravitate.energy/api)",
|
|
51
|
+
)
|
|
52
|
+
def login(mom_url):
|
|
53
|
+
"""Authenticate with mom via browser."""
|
|
54
|
+
client = MomClient(mom_url)
|
|
55
|
+
|
|
56
|
+
# Check if already logged in
|
|
57
|
+
if config_exists():
|
|
58
|
+
try:
|
|
59
|
+
config = load_config()
|
|
60
|
+
click.echo(f"Already logged in as {config.user_email}")
|
|
61
|
+
if click.confirm("Do you want to log in again?", default=False):
|
|
62
|
+
delete_config()
|
|
63
|
+
else:
|
|
64
|
+
return
|
|
65
|
+
except Exception:
|
|
66
|
+
# Config corrupted, proceed with login
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
# Step 1: Initiate device authorization
|
|
70
|
+
click.echo("Initiating authentication...")
|
|
71
|
+
|
|
72
|
+
# Get device name from hostname with fallback
|
|
73
|
+
try:
|
|
74
|
+
device_name = socket.gethostname()
|
|
75
|
+
if not device_name or device_name == "localhost":
|
|
76
|
+
device_name = "unknown-device"
|
|
77
|
+
except Exception:
|
|
78
|
+
device_name = "unknown-device"
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
device_auth = client.initiate_device_auth(device_name=device_name)
|
|
82
|
+
except APIError as e:
|
|
83
|
+
click.echo(f"Error: {e}", err=True)
|
|
84
|
+
sys.exit(1)
|
|
85
|
+
|
|
86
|
+
# Step 2: Display instructions
|
|
87
|
+
click.echo("\n" + "=" * 60)
|
|
88
|
+
click.echo("Please authorize this CLI tool:")
|
|
89
|
+
click.echo(f" 1. Opening browser to: {device_auth['verification_uri_complete']}")
|
|
90
|
+
click.echo(f" 2. Or manually visit: {device_auth['verification_uri']}")
|
|
91
|
+
click.echo(f" and enter code: {device_auth['user_code']}")
|
|
92
|
+
click.echo("=" * 60 + "\n")
|
|
93
|
+
|
|
94
|
+
# Step 3: Open browser automatically
|
|
95
|
+
try:
|
|
96
|
+
webbrowser.open(device_auth["verification_uri_complete"])
|
|
97
|
+
except Exception:
|
|
98
|
+
click.echo("Could not open browser automatically. Please visit the URL manually.")
|
|
99
|
+
|
|
100
|
+
# Step 4: Poll for authorization
|
|
101
|
+
device_code = device_auth["device_code"]
|
|
102
|
+
interval = device_auth["interval"]
|
|
103
|
+
expires_in = device_auth["expires_in"]
|
|
104
|
+
|
|
105
|
+
click.echo("Waiting for authorization...", nl=False)
|
|
106
|
+
|
|
107
|
+
start_time = time.time()
|
|
108
|
+
while time.time() - start_time < expires_in:
|
|
109
|
+
time.sleep(interval)
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
result = client.poll_device_auth(device_code)
|
|
113
|
+
except APIError as e:
|
|
114
|
+
if "expired" in str(e).lower():
|
|
115
|
+
click.echo(" ✗")
|
|
116
|
+
click.echo("\nError: Authorization timed out. Please try again.", err=True)
|
|
117
|
+
sys.exit(1)
|
|
118
|
+
# Continue polling on other errors
|
|
119
|
+
click.echo(".", nl=False)
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
if result.get("error"):
|
|
123
|
+
if result["error"] == "expired_token":
|
|
124
|
+
click.echo(" ✗")
|
|
125
|
+
click.echo("\nError: Authorization timed out. Please try again.", err=True)
|
|
126
|
+
sys.exit(1)
|
|
127
|
+
# Continue polling for other errors
|
|
128
|
+
click.echo(".", nl=False)
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
if result.get("authorized"):
|
|
132
|
+
click.echo(" ✓")
|
|
133
|
+
|
|
134
|
+
# Step 5: Extract credentials from response
|
|
135
|
+
user_email = result["user_email"]
|
|
136
|
+
token_id = result["token_id"]
|
|
137
|
+
refresh_token = result["refresh_token"]
|
|
138
|
+
refresh_expires_in = result["refresh_expires_in"]
|
|
139
|
+
device_name = result["device_name"] # Final device name (may have been edited by user)
|
|
140
|
+
|
|
141
|
+
# Save config (without mom_url - it's runtime only)
|
|
142
|
+
config = Config(
|
|
143
|
+
user_email=user_email,
|
|
144
|
+
refresh_token=refresh_token,
|
|
145
|
+
refresh_token_expires_at=datetime.now(UTC) + timedelta(seconds=refresh_expires_in),
|
|
146
|
+
token_id=token_id,
|
|
147
|
+
device_name=device_name,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
save_config(config)
|
|
152
|
+
except ConfigError as e:
|
|
153
|
+
click.echo(f"\nError saving config: {e}", err=True)
|
|
154
|
+
sys.exit(1)
|
|
155
|
+
|
|
156
|
+
click.echo(f"\n✓ Successfully logged in as {user_email}")
|
|
157
|
+
click.echo(f" Credentials saved to: {get_config_path()}")
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
click.echo(".", nl=False)
|
|
161
|
+
|
|
162
|
+
# Timeout
|
|
163
|
+
click.echo(" ✗")
|
|
164
|
+
click.echo("\nError: Authorization timed out or cancelled. Please try again.", err=True)
|
|
165
|
+
sys.exit(1)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@main.command()
|
|
169
|
+
def logout():
|
|
170
|
+
"""Clear local credentials and revoke token on backend."""
|
|
171
|
+
try:
|
|
172
|
+
config = load_config()
|
|
173
|
+
|
|
174
|
+
# Try to revoke token on backend
|
|
175
|
+
try:
|
|
176
|
+
mom_token = get_mom_token()
|
|
177
|
+
mom_url = get_mom_url()
|
|
178
|
+
client = MomClient(mom_url)
|
|
179
|
+
# Delete this specific token by token_id
|
|
180
|
+
client.delete_cli_token(config.token_id, mom_token)
|
|
181
|
+
click.echo("✓ Token revoked on server")
|
|
182
|
+
except Exception as e:
|
|
183
|
+
click.echo(f"⚠ Could not revoke token on server: {e}")
|
|
184
|
+
click.echo(" (Will still clear local credentials)")
|
|
185
|
+
|
|
186
|
+
# Delete local config file
|
|
187
|
+
config_path = get_config_path()
|
|
188
|
+
if delete_config():
|
|
189
|
+
click.echo(f"✓ Local credentials cleared from {config_path}")
|
|
190
|
+
else:
|
|
191
|
+
click.echo("No local credentials found")
|
|
192
|
+
|
|
193
|
+
except FileNotFoundError:
|
|
194
|
+
click.echo("✓ Logged out (no existing session)")
|
|
195
|
+
except Exception as e:
|
|
196
|
+
click.echo(f"Error: {e}", err=True)
|
|
197
|
+
sys.exit(1)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@main.command()
|
|
201
|
+
def status():
|
|
202
|
+
"""Show login status and token expiry."""
|
|
203
|
+
try:
|
|
204
|
+
config = load_config()
|
|
205
|
+
|
|
206
|
+
mom_url = get_mom_url()
|
|
207
|
+
|
|
208
|
+
click.echo("Gravi CLI Status")
|
|
209
|
+
click.echo("=" * 40)
|
|
210
|
+
click.echo(f"User: {config.user_email}")
|
|
211
|
+
click.echo(f"Mom URL: {mom_url}")
|
|
212
|
+
click.echo(f"Device: {config.device_name}")
|
|
213
|
+
click.echo(f"Token ID: {config.token_id}")
|
|
214
|
+
|
|
215
|
+
# Calculate expiry
|
|
216
|
+
# Handle both string (from config file) and datetime object
|
|
217
|
+
if isinstance(config.refresh_token_expires_at, str):
|
|
218
|
+
expires_at = datetime.fromisoformat(config.refresh_token_expires_at.replace("Z", "+00:00"))
|
|
219
|
+
else:
|
|
220
|
+
expires_at = config.refresh_token_expires_at
|
|
221
|
+
|
|
222
|
+
time_remaining = expires_at - datetime.now(UTC)
|
|
223
|
+
days_remaining = time_remaining.days
|
|
224
|
+
|
|
225
|
+
if days_remaining < 0:
|
|
226
|
+
click.echo("Token: ✗ EXPIRED")
|
|
227
|
+
click.echo("Action: Run 'gravi login' to re-authenticate")
|
|
228
|
+
elif days_remaining < 2:
|
|
229
|
+
click.echo(f"Token: ⚠ Expires in {days_remaining} day(s)")
|
|
230
|
+
click.echo("Action: Will auto-renew on next use")
|
|
231
|
+
else:
|
|
232
|
+
click.echo(f"Token: ✓ Valid for {days_remaining} more days")
|
|
233
|
+
|
|
234
|
+
except FileNotFoundError:
|
|
235
|
+
click.echo("Not logged in. Run 'gravi login' to authenticate.")
|
|
236
|
+
except Exception as e:
|
|
237
|
+
click.echo(f"Error: {e}", err=True)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@main.command()
|
|
241
|
+
def whoami():
|
|
242
|
+
"""Show current user info and mom URL."""
|
|
243
|
+
try:
|
|
244
|
+
config = load_config()
|
|
245
|
+
|
|
246
|
+
# Get fresh access token to verify we're still authenticated
|
|
247
|
+
mom_token = get_mom_token()
|
|
248
|
+
mom_url = get_mom_url()
|
|
249
|
+
|
|
250
|
+
click.echo(f"Logged in as: {config.user_email}")
|
|
251
|
+
click.echo(f"Mom URL: {mom_url}")
|
|
252
|
+
click.echo(f"Device: {config.device_name}")
|
|
253
|
+
|
|
254
|
+
except FileNotFoundError:
|
|
255
|
+
click.echo("Not logged in. Run 'gravi login' to authenticate.")
|
|
256
|
+
except NotAuthenticatedError:
|
|
257
|
+
click.echo("Session expired. Run 'gravi login' to re-authenticate.")
|
|
258
|
+
except Exception as e:
|
|
259
|
+
click.echo(f"Error: {e}", err=True)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
@main.group()
|
|
263
|
+
def tokens():
|
|
264
|
+
"""Manage CLI authorization tokens."""
|
|
265
|
+
pass
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
@tokens.command("list")
|
|
269
|
+
def tokens_list():
|
|
270
|
+
"""List all authorized CLI tokens."""
|
|
271
|
+
try:
|
|
272
|
+
config = load_config()
|
|
273
|
+
mom_token = get_mom_token()
|
|
274
|
+
mom_url = get_mom_url()
|
|
275
|
+
client = MomClient(mom_url)
|
|
276
|
+
|
|
277
|
+
response = client.list_cli_tokens(mom_token)
|
|
278
|
+
|
|
279
|
+
if not response["tokens"]:
|
|
280
|
+
click.echo("No active tokens found.")
|
|
281
|
+
return
|
|
282
|
+
|
|
283
|
+
click.echo("Active CLI Tokens:")
|
|
284
|
+
click.echo("=" * 80)
|
|
285
|
+
|
|
286
|
+
for token in response["tokens"]:
|
|
287
|
+
is_current = token["id"] == config.token_id
|
|
288
|
+
marker = "→" if is_current else " "
|
|
289
|
+
|
|
290
|
+
click.echo(f"{marker} ID: {token['id']}")
|
|
291
|
+
click.echo(f" Device: {token['device_name']}")
|
|
292
|
+
click.echo(f" Type: {token['token_type']}")
|
|
293
|
+
click.echo(f" Created: {token['created_at']}")
|
|
294
|
+
click.echo(f" Last used: {token.get('last_used_at', 'Never')}")
|
|
295
|
+
click.echo(f" Expires in: {token['days_until_expiry']} days")
|
|
296
|
+
if is_current:
|
|
297
|
+
click.echo(" (Current session)")
|
|
298
|
+
click.echo()
|
|
299
|
+
|
|
300
|
+
except NotAuthenticatedError as e:
|
|
301
|
+
click.echo(f"Error: {e}", err=True)
|
|
302
|
+
sys.exit(1)
|
|
303
|
+
except Exception as e:
|
|
304
|
+
click.echo(f"Error: {e}", err=True)
|
|
305
|
+
sys.exit(1)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
@tokens.command("revoke")
|
|
309
|
+
@click.argument("token_id", required=False)
|
|
310
|
+
@click.option("--all", is_flag=True, help="Revoke all tokens")
|
|
311
|
+
def tokens_revoke(token_id, all):
|
|
312
|
+
"""Revoke a CLI token by ID, or all tokens."""
|
|
313
|
+
try:
|
|
314
|
+
config = load_config()
|
|
315
|
+
mom_token = get_mom_token()
|
|
316
|
+
mom_url = get_mom_url()
|
|
317
|
+
client = MomClient(mom_url)
|
|
318
|
+
|
|
319
|
+
if all:
|
|
320
|
+
if not click.confirm(
|
|
321
|
+
"Are you sure you want to revoke ALL tokens? This will log you out everywhere.",
|
|
322
|
+
default=False,
|
|
323
|
+
):
|
|
324
|
+
click.echo("Cancelled.")
|
|
325
|
+
return
|
|
326
|
+
|
|
327
|
+
# Get all tokens and revoke them
|
|
328
|
+
response = client.list_cli_tokens(mom_token)
|
|
329
|
+
for token in response["tokens"]:
|
|
330
|
+
client.delete_cli_token(token["id"], mom_token)
|
|
331
|
+
click.echo(f"✓ Revoked token: {token['device_name']}")
|
|
332
|
+
|
|
333
|
+
# Clear local config
|
|
334
|
+
delete_config()
|
|
335
|
+
click.echo("\n✓ All tokens revoked. You are now logged out.")
|
|
336
|
+
|
|
337
|
+
elif token_id:
|
|
338
|
+
client.delete_cli_token(token_id, mom_token)
|
|
339
|
+
click.echo(f"✓ Token {token_id} revoked")
|
|
340
|
+
|
|
341
|
+
# If revoking current token, clear local config
|
|
342
|
+
# Compare as strings to handle both string and ObjectId types
|
|
343
|
+
if str(token_id) == str(config.token_id):
|
|
344
|
+
delete_config()
|
|
345
|
+
click.echo("✓ Current session ended. Run 'gravi login' to re-authenticate.")
|
|
346
|
+
|
|
347
|
+
else:
|
|
348
|
+
click.echo("Error: Specify a token ID or use --all flag", err=True)
|
|
349
|
+
click.echo("Usage: gravi tokens revoke <token_id>")
|
|
350
|
+
click.echo(" gravi tokens revoke --all")
|
|
351
|
+
sys.exit(1)
|
|
352
|
+
|
|
353
|
+
except NotAuthenticatedError as e:
|
|
354
|
+
click.echo(f"Error: {e}", err=True)
|
|
355
|
+
sys.exit(1)
|
|
356
|
+
except Exception as e:
|
|
357
|
+
click.echo(f"Error: {e}", err=True)
|
|
358
|
+
sys.exit(1)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
@main.command()
|
|
362
|
+
@click.argument("instance_key")
|
|
363
|
+
@click.option(
|
|
364
|
+
"--format",
|
|
365
|
+
type=click.Choice(["json", "env"]),
|
|
366
|
+
default="json",
|
|
367
|
+
help="Output format (default: json)",
|
|
368
|
+
)
|
|
369
|
+
def config(instance_key, format):
|
|
370
|
+
"""Get instance configuration (credentials, URLs, etc.)."""
|
|
371
|
+
try:
|
|
372
|
+
config_data = get_instance_config(instance_key)
|
|
373
|
+
|
|
374
|
+
if format == "json":
|
|
375
|
+
click.echo(json.dumps(config_data, indent=2))
|
|
376
|
+
elif format == "env":
|
|
377
|
+
# Flatten config for environment variables
|
|
378
|
+
click.echo(f"# Environment variables for {instance_key}")
|
|
379
|
+
click.echo(f"export INSTANCE_KEY={shlex.quote(config_data['instance_key'])}")
|
|
380
|
+
click.echo(f"export INSTANCE_NAME={shlex.quote(config_data['name'])}")
|
|
381
|
+
click.echo(f"export API_URL={shlex.quote(config_data['api_url'])}")
|
|
382
|
+
|
|
383
|
+
for key, value in config_data.get("config", {}).items():
|
|
384
|
+
env_key = key.upper()
|
|
385
|
+
# Quote values to handle special characters and spaces
|
|
386
|
+
click.echo(f"export {env_key}={shlex.quote(str(value))}")
|
|
387
|
+
|
|
388
|
+
except NotAuthenticatedError as e:
|
|
389
|
+
click.echo(f"Error: {e}", err=True)
|
|
390
|
+
sys.exit(1)
|
|
391
|
+
except Exception as e:
|
|
392
|
+
click.echo(f"Error: {e}", err=True)
|
|
393
|
+
sys.exit(1)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
# Add missing import for timedelta
|
|
397
|
+
from datetime import timedelta
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
if __name__ == "__main__":
|
|
401
|
+
main()
|