terrascout 0.1.0__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.
- terrascout-0.1.0/.gitignore +48 -0
- terrascout-0.1.0/PKG-INFO +125 -0
- terrascout-0.1.0/README.md +96 -0
- terrascout-0.1.0/pyproject.toml +49 -0
- terrascout-0.1.0/terrascout_cli/__init__.py +3 -0
- terrascout-0.1.0/terrascout_cli/client.py +52 -0
- terrascout-0.1.0/terrascout_cli/commands/__init__.py +0 -0
- terrascout-0.1.0/terrascout_cli/commands/agents.py +72 -0
- terrascout-0.1.0/terrascout_cli/commands/auth.py +91 -0
- terrascout-0.1.0/terrascout_cli/commands/config.py +45 -0
- terrascout-0.1.0/terrascout_cli/commands/project.py +109 -0
- terrascout-0.1.0/terrascout_cli/commands/scope.py +171 -0
- terrascout-0.1.0/terrascout_cli/commands/search.py +91 -0
- terrascout-0.1.0/terrascout_cli/commands/skills.py +90 -0
- terrascout-0.1.0/terrascout_cli/config.py +50 -0
- terrascout-0.1.0/terrascout_cli/main.py +71 -0
- terrascout-0.1.0/terrascout_cli/output.py +149 -0
- terrascout-0.1.0/tests/__init__.py +0 -0
- terrascout-0.1.0/tests/conftest.py +25 -0
- terrascout-0.1.0/tests/test_cli.py +343 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Environment
|
|
2
|
+
.env
|
|
3
|
+
.env.local
|
|
4
|
+
.env.*.local
|
|
5
|
+
|
|
6
|
+
# Python
|
|
7
|
+
__pycache__/
|
|
8
|
+
*.py[cod]
|
|
9
|
+
*$py.class
|
|
10
|
+
*.so
|
|
11
|
+
venv/
|
|
12
|
+
.venv/
|
|
13
|
+
*.egg-info/
|
|
14
|
+
dist/
|
|
15
|
+
build/
|
|
16
|
+
|
|
17
|
+
# IDE
|
|
18
|
+
.vscode/
|
|
19
|
+
.idea/
|
|
20
|
+
*.swp
|
|
21
|
+
*.swo
|
|
22
|
+
*~
|
|
23
|
+
|
|
24
|
+
# OS
|
|
25
|
+
.DS_Store
|
|
26
|
+
Thumbs.db
|
|
27
|
+
|
|
28
|
+
# Node (frontend)
|
|
29
|
+
node_modules/
|
|
30
|
+
frontend/dist/
|
|
31
|
+
|
|
32
|
+
# Docker
|
|
33
|
+
*.log
|
|
34
|
+
|
|
35
|
+
# Testing
|
|
36
|
+
.pytest_cache/
|
|
37
|
+
.coverage
|
|
38
|
+
htmlcov/
|
|
39
|
+
|
|
40
|
+
# Playwright
|
|
41
|
+
frontend/e2e/screenshots/
|
|
42
|
+
frontend/playwright-report/
|
|
43
|
+
frontend/test-results/
|
|
44
|
+
docs/screenshots/
|
|
45
|
+
|
|
46
|
+
# Distribution
|
|
47
|
+
*.egg
|
|
48
|
+
*.whl
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: terrascout
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Terrascout CLI — geospatial data scoping from the terminal
|
|
5
|
+
Project-URL: Homepage, http://130.162.194.71
|
|
6
|
+
Project-URL: Documentation, http://130.162.194.71/integrations
|
|
7
|
+
Project-URL: Release Notes, http://130.162.194.71/integrations
|
|
8
|
+
Project-URL: Support, http://130.162.194.71/integrations
|
|
9
|
+
Author-email: Mazdak Ghasemi Tootkaboni <maz@terrascout.dev>
|
|
10
|
+
License: Proprietary
|
|
11
|
+
Keywords: cli,earth-observation,geospatial,remote-sensing,satellite,terrascout
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Science/Research
|
|
16
|
+
Classifier: License :: Other/Proprietary License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Requires-Dist: httpx>=0.27
|
|
25
|
+
Requires-Dist: pyyaml>=6.0
|
|
26
|
+
Requires-Dist: rich>=13.0
|
|
27
|
+
Requires-Dist: typer>=0.12
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# Terrascout CLI
|
|
31
|
+
|
|
32
|
+
Geospatial data scoping from the terminal. Find the best satellite imagery and spatial datasets for your project — ranked, scored, and assessed for feasibility.
|
|
33
|
+
|
|
34
|
+
Powered by [Terrascout](http://130.162.194.71).
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install terrascout
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Requires Python 3.11+. No API key needed for free-tier features.
|
|
43
|
+
|
|
44
|
+
## Quick Start
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# Check connectivity to Terrascout API
|
|
48
|
+
terrascout health
|
|
49
|
+
|
|
50
|
+
# Create a scope from a natural language brief
|
|
51
|
+
terrascout scope new "Monitor vegetation on a 500ha mine site near Emerald QLD, monthly for 12 months"
|
|
52
|
+
|
|
53
|
+
# View results for a scope
|
|
54
|
+
terrascout scope view <scope_id>
|
|
55
|
+
|
|
56
|
+
# Download a PDF compliance report
|
|
57
|
+
terrascout scope report <scope_id> --output report.pdf
|
|
58
|
+
|
|
59
|
+
# Compare recommendations side-by-side
|
|
60
|
+
terrascout scope compare <scope_id>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Search
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# Browse free satellite data providers
|
|
67
|
+
terrascout search providers --free-only
|
|
68
|
+
|
|
69
|
+
# Filter by resolution
|
|
70
|
+
terrascout search providers --resolution 10
|
|
71
|
+
|
|
72
|
+
# Search STAC catalogs for recent scenes
|
|
73
|
+
terrascout search stac --collection sentinel-2-l2a --bbox 144,-38,145,-37
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Projects
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
# List your project workspaces
|
|
80
|
+
terrascout project list
|
|
81
|
+
|
|
82
|
+
# Create a new project
|
|
83
|
+
terrascout project create "Mine Rehabilitation Monitoring"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## AI Agents
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
# List available AI agents
|
|
90
|
+
terrascout agents list
|
|
91
|
+
|
|
92
|
+
# Run an agent on a scope
|
|
93
|
+
terrascout agents run methodology_writer --scope <scope_id>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Configuration
|
|
97
|
+
|
|
98
|
+
Config is stored at `~/.terrascout/config.yaml`:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
# View current config
|
|
102
|
+
terrascout config show
|
|
103
|
+
|
|
104
|
+
# Point to a different API (e.g., local dev server)
|
|
105
|
+
terrascout config set api_url http://localhost:8000
|
|
106
|
+
|
|
107
|
+
# Point to production (default)
|
|
108
|
+
terrascout config set api_url http://130.162.194.71
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Authentication
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
terrascout auth login --email user@example.com
|
|
115
|
+
terrascout auth status
|
|
116
|
+
terrascout auth logout
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Authentication is optional for core scoping features. Required for projects, agents, and alert subscriptions.
|
|
120
|
+
|
|
121
|
+
## Also Available
|
|
122
|
+
|
|
123
|
+
- **Web app**: [http://130.162.194.71](http://130.162.194.71)
|
|
124
|
+
- **MCP Server**: Connect Terrascout to Claude, Cursor, Windsurf, and other AI assistants
|
|
125
|
+
- **REST API**: Full OpenAPI docs at [/docs](http://130.162.194.71/docs)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Terrascout CLI
|
|
2
|
+
|
|
3
|
+
Geospatial data scoping from the terminal. Find the best satellite imagery and spatial datasets for your project — ranked, scored, and assessed for feasibility.
|
|
4
|
+
|
|
5
|
+
Powered by [Terrascout](http://130.162.194.71).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install terrascout
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Requires Python 3.11+. No API key needed for free-tier features.
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# Check connectivity to Terrascout API
|
|
19
|
+
terrascout health
|
|
20
|
+
|
|
21
|
+
# Create a scope from a natural language brief
|
|
22
|
+
terrascout scope new "Monitor vegetation on a 500ha mine site near Emerald QLD, monthly for 12 months"
|
|
23
|
+
|
|
24
|
+
# View results for a scope
|
|
25
|
+
terrascout scope view <scope_id>
|
|
26
|
+
|
|
27
|
+
# Download a PDF compliance report
|
|
28
|
+
terrascout scope report <scope_id> --output report.pdf
|
|
29
|
+
|
|
30
|
+
# Compare recommendations side-by-side
|
|
31
|
+
terrascout scope compare <scope_id>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Search
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# Browse free satellite data providers
|
|
38
|
+
terrascout search providers --free-only
|
|
39
|
+
|
|
40
|
+
# Filter by resolution
|
|
41
|
+
terrascout search providers --resolution 10
|
|
42
|
+
|
|
43
|
+
# Search STAC catalogs for recent scenes
|
|
44
|
+
terrascout search stac --collection sentinel-2-l2a --bbox 144,-38,145,-37
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Projects
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# List your project workspaces
|
|
51
|
+
terrascout project list
|
|
52
|
+
|
|
53
|
+
# Create a new project
|
|
54
|
+
terrascout project create "Mine Rehabilitation Monitoring"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## AI Agents
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# List available AI agents
|
|
61
|
+
terrascout agents list
|
|
62
|
+
|
|
63
|
+
# Run an agent on a scope
|
|
64
|
+
terrascout agents run methodology_writer --scope <scope_id>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Configuration
|
|
68
|
+
|
|
69
|
+
Config is stored at `~/.terrascout/config.yaml`:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# View current config
|
|
73
|
+
terrascout config show
|
|
74
|
+
|
|
75
|
+
# Point to a different API (e.g., local dev server)
|
|
76
|
+
terrascout config set api_url http://localhost:8000
|
|
77
|
+
|
|
78
|
+
# Point to production (default)
|
|
79
|
+
terrascout config set api_url http://130.162.194.71
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Authentication
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
terrascout auth login --email user@example.com
|
|
86
|
+
terrascout auth status
|
|
87
|
+
terrascout auth logout
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Authentication is optional for core scoping features. Required for projects, agents, and alert subscriptions.
|
|
91
|
+
|
|
92
|
+
## Also Available
|
|
93
|
+
|
|
94
|
+
- **Web app**: [http://130.162.194.71](http://130.162.194.71)
|
|
95
|
+
- **MCP Server**: Connect Terrascout to Claude, Cursor, Windsurf, and other AI assistants
|
|
96
|
+
- **REST API**: Full OpenAPI docs at [/docs](http://130.162.194.71/docs)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "terrascout"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Terrascout CLI — geospatial data scoping from the terminal"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "Proprietary"}
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Mazdak Ghasemi Tootkaboni", email = "maz@terrascout.dev" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["geospatial", "satellite", "remote-sensing", "cli", "terrascout", "earth-observation"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Environment :: Console",
|
|
19
|
+
"Intended Audience :: Science/Research",
|
|
20
|
+
"Intended Audience :: Developers",
|
|
21
|
+
"Topic :: Scientific/Engineering :: GIS",
|
|
22
|
+
"License :: Other/Proprietary License",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Typing :: Typed",
|
|
28
|
+
]
|
|
29
|
+
dependencies = [
|
|
30
|
+
"typer>=0.12",
|
|
31
|
+
"rich>=13.0",
|
|
32
|
+
"httpx>=0.27",
|
|
33
|
+
"pyyaml>=6.0",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[project.scripts]
|
|
37
|
+
terrascout = "terrascout_cli.main:app"
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "http://130.162.194.71"
|
|
41
|
+
Documentation = "http://130.162.194.71/integrations"
|
|
42
|
+
"Release Notes" = "http://130.162.194.71/integrations"
|
|
43
|
+
"Support" = "http://130.162.194.71/integrations"
|
|
44
|
+
|
|
45
|
+
[tool.hatch.build.targets.wheel]
|
|
46
|
+
packages = ["terrascout_cli"]
|
|
47
|
+
|
|
48
|
+
[tool.pytest.ini_options]
|
|
49
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""HTTP API client — all backend communication goes through here."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from terrascout_cli.config import get_api_url, get_token
|
|
8
|
+
|
|
9
|
+
TIMEOUT = 30.0
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _headers() -> dict[str, str]:
|
|
13
|
+
headers: dict[str, str] = {"Accept": "application/json"}
|
|
14
|
+
token = get_token()
|
|
15
|
+
if token:
|
|
16
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
17
|
+
return headers
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _base() -> str:
|
|
21
|
+
return get_api_url().rstrip("/")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _url(path: str) -> str:
|
|
25
|
+
return f"{_base()}/api/v1{path}"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get(path: str, params: dict[str, Any] | None = None) -> httpx.Response:
|
|
29
|
+
"""GET request to the Terrascout API."""
|
|
30
|
+
return httpx.get(_url(path), params=params, headers=_headers(), timeout=TIMEOUT)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def post(path: str, json: dict[str, Any] | None = None) -> httpx.Response:
|
|
34
|
+
"""POST request to the Terrascout API."""
|
|
35
|
+
return httpx.post(_url(path), json=json, headers=_headers(), timeout=TIMEOUT)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_raw(path: str, params: dict[str, Any] | None = None) -> httpx.Response:
|
|
39
|
+
"""GET request returning raw bytes (for PDF downloads etc.)."""
|
|
40
|
+
return httpx.get(
|
|
41
|
+
_url(path),
|
|
42
|
+
params=params,
|
|
43
|
+
headers={**_headers(), "Accept": "application/pdf"},
|
|
44
|
+
timeout=60.0,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def health() -> dict[str, Any]:
|
|
49
|
+
"""Check API health."""
|
|
50
|
+
resp = get("/health")
|
|
51
|
+
resp.raise_for_status()
|
|
52
|
+
return resp.json()
|
|
File without changes
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Agents commands — list, run."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from rich.panel import Panel
|
|
5
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
6
|
+
|
|
7
|
+
from terrascout_cli import client, output
|
|
8
|
+
|
|
9
|
+
app = typer.Typer(no_args_is_help=True)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@app.command("list")
|
|
13
|
+
def agents_list() -> None:
|
|
14
|
+
"""List available AI agents."""
|
|
15
|
+
with Progress(
|
|
16
|
+
SpinnerColumn(),
|
|
17
|
+
TextColumn("[bold cyan]Loading agents..."),
|
|
18
|
+
transient=True,
|
|
19
|
+
console=output.console,
|
|
20
|
+
):
|
|
21
|
+
resp = client.get("/agents")
|
|
22
|
+
|
|
23
|
+
if resp.status_code != 200:
|
|
24
|
+
output.error(f"API error {resp.status_code}: {resp.text[:200]}")
|
|
25
|
+
raise typer.Exit(1)
|
|
26
|
+
|
|
27
|
+
data = resp.json()
|
|
28
|
+
agents = data if isinstance(data, list) else data.get("agents", [])
|
|
29
|
+
|
|
30
|
+
if agents:
|
|
31
|
+
output.console.print(output.agents_table(agents))
|
|
32
|
+
else:
|
|
33
|
+
output.warning("No agents available.")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@app.command("run")
|
|
37
|
+
def agents_run(
|
|
38
|
+
slug: str = typer.Argument(help="Agent slug (e.g. methodology_writer)"),
|
|
39
|
+
scope_id: str = typer.Option(..., "--scope", "-s", help="Scope UUID for context"),
|
|
40
|
+
prompt: str = typer.Option("", "--prompt", "-p", help="Additional prompt/instructions"),
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Execute an AI agent with scope context."""
|
|
43
|
+
body: dict = {"scope_id": scope_id}
|
|
44
|
+
if prompt:
|
|
45
|
+
body["prompt"] = prompt
|
|
46
|
+
|
|
47
|
+
with Progress(
|
|
48
|
+
SpinnerColumn(),
|
|
49
|
+
TextColumn(f"[bold cyan]Running agent '{slug}'..."),
|
|
50
|
+
transient=True,
|
|
51
|
+
console=output.console,
|
|
52
|
+
):
|
|
53
|
+
resp = client.post(f"/agents/{slug}/run", json=body)
|
|
54
|
+
|
|
55
|
+
if resp.status_code == 401:
|
|
56
|
+
output.error("Authentication required. Run [bold]terrascout auth login[/].")
|
|
57
|
+
raise typer.Exit(1)
|
|
58
|
+
if resp.status_code == 403:
|
|
59
|
+
output.error("Tier upgrade required for this agent.")
|
|
60
|
+
raise typer.Exit(1)
|
|
61
|
+
if resp.status_code != 200:
|
|
62
|
+
output.error(f"API error {resp.status_code}: {resp.text[:200]}")
|
|
63
|
+
raise typer.Exit(1)
|
|
64
|
+
|
|
65
|
+
data = resp.json()
|
|
66
|
+
agent_output = data.get("output", data.get("result", ""))
|
|
67
|
+
|
|
68
|
+
output.console.print(Panel(
|
|
69
|
+
str(agent_output),
|
|
70
|
+
title=f"Agent: {slug}",
|
|
71
|
+
border_style="green",
|
|
72
|
+
))
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Auth commands — login, logout, status."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
5
|
+
|
|
6
|
+
from terrascout_cli import client, output
|
|
7
|
+
from terrascout_cli.config import get_token, set_token
|
|
8
|
+
|
|
9
|
+
app = typer.Typer(no_args_is_help=True)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@app.command("login")
|
|
13
|
+
def auth_login(
|
|
14
|
+
email: str = typer.Option(..., "--email", "-e", prompt=True, help="Account email"),
|
|
15
|
+
password: str = typer.Option(..., "--password", "-p", prompt=True, hide_input=True, help="Password"),
|
|
16
|
+
) -> None:
|
|
17
|
+
"""Log in to Terrascout and store JWT token."""
|
|
18
|
+
import httpx
|
|
19
|
+
|
|
20
|
+
from terrascout_cli.config import get_api_url
|
|
21
|
+
|
|
22
|
+
with Progress(
|
|
23
|
+
SpinnerColumn(),
|
|
24
|
+
TextColumn("[bold cyan]Authenticating..."),
|
|
25
|
+
transient=True,
|
|
26
|
+
console=output.console,
|
|
27
|
+
):
|
|
28
|
+
# Call Supabase-compatible auth endpoint
|
|
29
|
+
# The frontend uses Supabase JS SDK, but the CLI can use the REST API
|
|
30
|
+
try:
|
|
31
|
+
base = get_api_url().rstrip("/")
|
|
32
|
+
resp = httpx.post(
|
|
33
|
+
f"{base}/api/v1/auth/login",
|
|
34
|
+
json={"email": email, "password": password},
|
|
35
|
+
timeout=15.0,
|
|
36
|
+
)
|
|
37
|
+
except httpx.RequestError as exc:
|
|
38
|
+
output.error(f"Connection failed: {exc}")
|
|
39
|
+
raise typer.Exit(1)
|
|
40
|
+
|
|
41
|
+
if resp.status_code == 200:
|
|
42
|
+
data = resp.json()
|
|
43
|
+
token = data.get("access_token") or data.get("token")
|
|
44
|
+
if token:
|
|
45
|
+
set_token(token)
|
|
46
|
+
output.success(f"Logged in as [bold]{email}[/]")
|
|
47
|
+
else:
|
|
48
|
+
output.error("Login succeeded but no token returned.")
|
|
49
|
+
raise typer.Exit(1)
|
|
50
|
+
elif resp.status_code == 401:
|
|
51
|
+
output.error("Invalid email or password.")
|
|
52
|
+
raise typer.Exit(1)
|
|
53
|
+
else:
|
|
54
|
+
output.error(f"Login failed ({resp.status_code}): {resp.text[:200]}")
|
|
55
|
+
raise typer.Exit(1)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@app.command("logout")
|
|
59
|
+
def auth_logout() -> None:
|
|
60
|
+
"""Clear stored authentication token."""
|
|
61
|
+
set_token(None)
|
|
62
|
+
output.success("Logged out.")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@app.command("status")
|
|
66
|
+
def auth_status() -> None:
|
|
67
|
+
"""Show current authentication status."""
|
|
68
|
+
token = get_token()
|
|
69
|
+
if not token:
|
|
70
|
+
output.warning("Not logged in. Use [bold]terrascout auth login[/] to authenticate.")
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
# Try fetching profile to verify token is valid
|
|
74
|
+
with Progress(
|
|
75
|
+
SpinnerColumn(),
|
|
76
|
+
TextColumn("[bold cyan]Checking token..."),
|
|
77
|
+
transient=True,
|
|
78
|
+
console=output.console,
|
|
79
|
+
):
|
|
80
|
+
resp = client.get("/profile")
|
|
81
|
+
|
|
82
|
+
if resp.status_code == 200:
|
|
83
|
+
profile = resp.json()
|
|
84
|
+
email = profile.get("email", "?")
|
|
85
|
+
tier = profile.get("tier", "free")
|
|
86
|
+
output.success(f"Authenticated as [bold]{email}[/] (tier: {tier})")
|
|
87
|
+
elif resp.status_code == 401:
|
|
88
|
+
output.error("Token expired or invalid. Run [bold]terrascout auth login[/] again.")
|
|
89
|
+
set_token(None)
|
|
90
|
+
else:
|
|
91
|
+
output.warning(f"Could not verify token (status {resp.status_code})")
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Config commands — show, set."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from rich.table import Table
|
|
5
|
+
|
|
6
|
+
from terrascout_cli import output
|
|
7
|
+
from terrascout_cli.config import CONFIG_FILE, load_config, save_config
|
|
8
|
+
|
|
9
|
+
app = typer.Typer(no_args_is_help=True)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@app.command("show")
|
|
13
|
+
def config_show() -> None:
|
|
14
|
+
"""Display current configuration."""
|
|
15
|
+
cfg = load_config()
|
|
16
|
+
|
|
17
|
+
table = Table(title=f"Config ({CONFIG_FILE})", border_style="cyan")
|
|
18
|
+
table.add_column("Key", style="bold white")
|
|
19
|
+
table.add_column("Value", style="cyan")
|
|
20
|
+
|
|
21
|
+
for key, value in cfg.items():
|
|
22
|
+
display = str(value) if value is not None else "[dim]not set[/]"
|
|
23
|
+
if key == "token" and value:
|
|
24
|
+
display = f"{value[:20]}..." if len(value) > 20 else value
|
|
25
|
+
table.add_row(key, display)
|
|
26
|
+
|
|
27
|
+
output.console.print(table)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@app.command("set")
|
|
31
|
+
def config_set(
|
|
32
|
+
key: str = typer.Argument(help="Config key (e.g. api_url)"),
|
|
33
|
+
value: str = typer.Argument(help="New value"),
|
|
34
|
+
) -> None:
|
|
35
|
+
"""Set a configuration value."""
|
|
36
|
+
cfg = load_config()
|
|
37
|
+
|
|
38
|
+
valid_keys = {"api_url", "token"}
|
|
39
|
+
if key not in valid_keys:
|
|
40
|
+
output.error(f"Unknown key '{key}'. Valid keys: {', '.join(sorted(valid_keys))}")
|
|
41
|
+
raise typer.Exit(1)
|
|
42
|
+
|
|
43
|
+
cfg[key] = value
|
|
44
|
+
save_config(cfg)
|
|
45
|
+
output.success(f"Set [bold]{key}[/] = {value}")
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Project commands — list, create, add-scope."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
|
|
9
|
+
from terrascout_cli import client, output
|
|
10
|
+
|
|
11
|
+
app = typer.Typer(no_args_is_help=True)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@app.command("list")
|
|
15
|
+
def project_list() -> None:
|
|
16
|
+
"""List your projects."""
|
|
17
|
+
with Progress(
|
|
18
|
+
SpinnerColumn(),
|
|
19
|
+
TextColumn("[bold cyan]Loading projects..."),
|
|
20
|
+
transient=True,
|
|
21
|
+
console=output.console,
|
|
22
|
+
):
|
|
23
|
+
resp = client.get("/projects")
|
|
24
|
+
|
|
25
|
+
if resp.status_code == 401:
|
|
26
|
+
output.error("Authentication required. Run [bold]terrascout auth login[/].")
|
|
27
|
+
raise typer.Exit(1)
|
|
28
|
+
if resp.status_code != 200:
|
|
29
|
+
output.error(f"API error {resp.status_code}: {resp.text[:200]}")
|
|
30
|
+
raise typer.Exit(1)
|
|
31
|
+
|
|
32
|
+
projects = resp.json()
|
|
33
|
+
if isinstance(projects, dict):
|
|
34
|
+
projects = projects.get("projects", [])
|
|
35
|
+
|
|
36
|
+
if not projects:
|
|
37
|
+
output.warning("No projects yet. Use [bold]terrascout project create[/] to start one.")
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
table = Table(title="Projects", border_style="cyan", show_lines=True)
|
|
41
|
+
table.add_column("ID", style="dim", max_width=36)
|
|
42
|
+
table.add_column("Name", style="bold white")
|
|
43
|
+
table.add_column("Status", style="cyan")
|
|
44
|
+
table.add_column("Scopes", justify="right")
|
|
45
|
+
table.add_column("Created", style="dim")
|
|
46
|
+
|
|
47
|
+
for p in projects:
|
|
48
|
+
table.add_row(
|
|
49
|
+
str(p.get("id", ""))[:36],
|
|
50
|
+
p.get("name", ""),
|
|
51
|
+
p.get("status", "active"),
|
|
52
|
+
str(p.get("scope_count", 0)),
|
|
53
|
+
str(p.get("created_at", ""))[:10],
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
output.console.print(table)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@app.command("create")
|
|
60
|
+
def project_create(
|
|
61
|
+
name: str = typer.Argument(help="Project name"),
|
|
62
|
+
description: Optional[str] = typer.Option(None, "--desc", "-d", help="Project description"),
|
|
63
|
+
) -> None:
|
|
64
|
+
"""Create a new project workspace."""
|
|
65
|
+
body: dict = {"name": name}
|
|
66
|
+
if description:
|
|
67
|
+
body["description"] = description
|
|
68
|
+
|
|
69
|
+
with Progress(
|
|
70
|
+
SpinnerColumn(),
|
|
71
|
+
TextColumn("[bold cyan]Creating project..."),
|
|
72
|
+
transient=True,
|
|
73
|
+
console=output.console,
|
|
74
|
+
):
|
|
75
|
+
resp = client.post("/projects", json=body)
|
|
76
|
+
|
|
77
|
+
if resp.status_code == 401:
|
|
78
|
+
output.error("Authentication required. Run [bold]terrascout auth login[/].")
|
|
79
|
+
raise typer.Exit(1)
|
|
80
|
+
if resp.status_code not in (200, 201):
|
|
81
|
+
output.error(f"API error {resp.status_code}: {resp.text[:200]}")
|
|
82
|
+
raise typer.Exit(1)
|
|
83
|
+
|
|
84
|
+
data = resp.json()
|
|
85
|
+
output.success(f"Project created: [bold]{data.get('name', name)}[/] ({data.get('id', '?')})")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@app.command("add-scope")
|
|
89
|
+
def project_add_scope(
|
|
90
|
+
project_id: str = typer.Argument(help="Project UUID"),
|
|
91
|
+
scope_id: str = typer.Argument(help="Scope UUID to add"),
|
|
92
|
+
) -> None:
|
|
93
|
+
"""Add a scope to an existing project."""
|
|
94
|
+
with Progress(
|
|
95
|
+
SpinnerColumn(),
|
|
96
|
+
TextColumn("[bold cyan]Adding scope to project..."),
|
|
97
|
+
transient=True,
|
|
98
|
+
console=output.console,
|
|
99
|
+
):
|
|
100
|
+
resp = client.post(f"/projects/{project_id}/scopes", json={"scope_id": scope_id})
|
|
101
|
+
|
|
102
|
+
if resp.status_code == 401:
|
|
103
|
+
output.error("Authentication required.")
|
|
104
|
+
raise typer.Exit(1)
|
|
105
|
+
if resp.status_code not in (200, 201):
|
|
106
|
+
output.error(f"API error {resp.status_code}: {resp.text[:200]}")
|
|
107
|
+
raise typer.Exit(1)
|
|
108
|
+
|
|
109
|
+
output.success(f"Scope [bold]{scope_id}[/] added to project [bold]{project_id}[/]")
|