recce-cloud 1.32.0__py3-none-any.whl → 1.33.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.
- recce_cloud/VERSION +1 -1
- recce_cloud/api/client.py +245 -2
- recce_cloud/auth/__init__.py +21 -0
- recce_cloud/auth/callback_server.py +128 -0
- recce_cloud/auth/login.py +281 -0
- recce_cloud/auth/profile.py +131 -0
- recce_cloud/auth/templates/error.html +58 -0
- recce_cloud/auth/templates/success.html +58 -0
- recce_cloud/cli.py +661 -33
- recce_cloud/commands/__init__.py +1 -0
- recce_cloud/commands/diagnostics.py +174 -0
- recce_cloud/config/__init__.py +19 -0
- recce_cloud/config/project_config.py +187 -0
- recce_cloud/config/resolver.py +137 -0
- recce_cloud/services/__init__.py +1 -0
- recce_cloud/services/diagnostic_service.py +380 -0
- recce_cloud/upload.py +211 -0
- {recce_cloud-1.32.0.dist-info → recce_cloud-1.33.1.dist-info}/METADATA +112 -2
- recce_cloud-1.33.1.dist-info/RECORD +37 -0
- recce_cloud-1.32.0.dist-info/RECORD +0 -24
- {recce_cloud-1.32.0.dist-info → recce_cloud-1.33.1.dist-info}/WHEEL +0 -0
- {recce_cloud-1.32.0.dist-info → recce_cloud-1.33.1.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# CLI commands - presentation layer
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Diagnostics CLI commands.
|
|
3
|
+
|
|
4
|
+
This module contains CLI presentation logic for diagnostic commands,
|
|
5
|
+
delegating business logic to the diagnostic service.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.panel import Panel
|
|
14
|
+
|
|
15
|
+
from recce_cloud.services.diagnostic_service import (
|
|
16
|
+
CheckStatus,
|
|
17
|
+
DiagnosticResults,
|
|
18
|
+
DiagnosticService,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DiagnosticRenderer:
|
|
23
|
+
"""Renders diagnostic results to the console."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, console: Console):
|
|
26
|
+
self.console = console
|
|
27
|
+
|
|
28
|
+
def render_header(self) -> None:
|
|
29
|
+
"""Render the diagnostic header."""
|
|
30
|
+
header = Panel(
|
|
31
|
+
"[bold]🩺 Recce Doctor[/bold]\n[dim]Checking your Recce Cloud setup...[/dim]",
|
|
32
|
+
expand=False,
|
|
33
|
+
padding=(0, 3),
|
|
34
|
+
)
|
|
35
|
+
self.console.print()
|
|
36
|
+
self.console.print(header)
|
|
37
|
+
self.console.print()
|
|
38
|
+
self.console.print("━" * 65)
|
|
39
|
+
self.console.print()
|
|
40
|
+
|
|
41
|
+
def render_login_check(self, results: DiagnosticResults) -> None:
|
|
42
|
+
"""Render the login status check."""
|
|
43
|
+
self.console.print("[bold]1. Login Status[/bold] ($ recce-cloud login)")
|
|
44
|
+
check = results.login
|
|
45
|
+
|
|
46
|
+
if check.passed:
|
|
47
|
+
email = check.details.get("email", "Unknown")
|
|
48
|
+
self.console.print(f"[green]✓[/green] Logged in as [cyan]{email}[/cyan]")
|
|
49
|
+
else:
|
|
50
|
+
self._render_failure(check)
|
|
51
|
+
|
|
52
|
+
self.console.print()
|
|
53
|
+
|
|
54
|
+
def render_project_binding_check(self, results: DiagnosticResults) -> None:
|
|
55
|
+
"""Render the project binding check."""
|
|
56
|
+
self.console.print("[bold]2. Project Binding[/bold] ($ recce-cloud init)")
|
|
57
|
+
check = results.project_binding
|
|
58
|
+
|
|
59
|
+
if check.passed:
|
|
60
|
+
org = check.details.get("org")
|
|
61
|
+
project = check.details.get("project")
|
|
62
|
+
source = check.details.get("source")
|
|
63
|
+
source_label = " (via env vars)" if source == "env_vars" else ""
|
|
64
|
+
self.console.print(f"[green]✓[/green] Bound to [cyan]{org}/{project}[/cyan]{source_label}")
|
|
65
|
+
else:
|
|
66
|
+
self._render_failure(check)
|
|
67
|
+
|
|
68
|
+
self.console.print()
|
|
69
|
+
|
|
70
|
+
def render_production_check(self, results: DiagnosticResults) -> None:
|
|
71
|
+
"""Render the production metadata check."""
|
|
72
|
+
self.console.print("[bold]3. Production Metadata[/bold]")
|
|
73
|
+
check = results.production_metadata
|
|
74
|
+
|
|
75
|
+
if check.status == CheckStatus.SKIP:
|
|
76
|
+
self.console.print(f"[yellow]⚠[/yellow] {check.message}")
|
|
77
|
+
elif check.passed:
|
|
78
|
+
session_name = check.details.get("session_name", "(unnamed)")
|
|
79
|
+
relative_time = check.details.get("relative_time")
|
|
80
|
+
time_str = f" (uploaded {relative_time})" if relative_time else ""
|
|
81
|
+
self.console.print(f'[green]✓[/green] Found production session "[cyan]{session_name}[/cyan]"{time_str}')
|
|
82
|
+
else:
|
|
83
|
+
self._render_failure(check)
|
|
84
|
+
|
|
85
|
+
self.console.print()
|
|
86
|
+
|
|
87
|
+
def render_dev_session_check(self, results: DiagnosticResults) -> None:
|
|
88
|
+
"""Render the dev session check."""
|
|
89
|
+
self.console.print("[bold]4. Dev Session[/bold]")
|
|
90
|
+
check = results.dev_session
|
|
91
|
+
|
|
92
|
+
if check.status == CheckStatus.SKIP:
|
|
93
|
+
self.console.print(f"[yellow]⚠[/yellow] {check.message}")
|
|
94
|
+
elif check.passed:
|
|
95
|
+
session_name = check.details.get("session_name", "(unnamed)")
|
|
96
|
+
relative_time = check.details.get("relative_time")
|
|
97
|
+
time_str = f" (uploaded {relative_time})" if relative_time else ""
|
|
98
|
+
self.console.print(f'[green]✓[/green] Found dev session "[cyan]{session_name}[/cyan]"{time_str}')
|
|
99
|
+
else:
|
|
100
|
+
self._render_failure(check)
|
|
101
|
+
|
|
102
|
+
def render_summary(self, results: DiagnosticResults) -> None:
|
|
103
|
+
"""Render the summary section."""
|
|
104
|
+
self.console.print()
|
|
105
|
+
self.console.print("━" * 65)
|
|
106
|
+
self.console.print()
|
|
107
|
+
|
|
108
|
+
if results.all_passed:
|
|
109
|
+
self.console.print("[green]✓ All checks passed![/green] Your Recce setup is ready.")
|
|
110
|
+
self.console.print()
|
|
111
|
+
self.console.print("Next step:")
|
|
112
|
+
self.console.print(" $ recce-cloud review --session-name <session_name>")
|
|
113
|
+
else:
|
|
114
|
+
self.console.print(
|
|
115
|
+
f"[yellow]⚠ {results.passed_count}/{results.total_count} checks passed.[/yellow] "
|
|
116
|
+
"See above for remediation steps."
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def render_all(self, results: DiagnosticResults) -> None:
|
|
120
|
+
"""Render all diagnostic results."""
|
|
121
|
+
self.render_header()
|
|
122
|
+
self.render_login_check(results)
|
|
123
|
+
self.render_project_binding_check(results)
|
|
124
|
+
self.render_production_check(results)
|
|
125
|
+
self.render_dev_session_check(results)
|
|
126
|
+
self.render_summary(results)
|
|
127
|
+
|
|
128
|
+
def _render_failure(self, check) -> None:
|
|
129
|
+
"""Render a failed check with its suggestion."""
|
|
130
|
+
self.console.print(f"[red]✗[/red] {check.message}")
|
|
131
|
+
if check.suggestion:
|
|
132
|
+
self.console.print()
|
|
133
|
+
self.console.print("[dim]→ To fix:[/dim]")
|
|
134
|
+
for line in check.suggestion.split("\n"):
|
|
135
|
+
self.console.print(f" {line}")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@click.command()
|
|
139
|
+
@click.option(
|
|
140
|
+
"--json",
|
|
141
|
+
"output_json",
|
|
142
|
+
is_flag=True,
|
|
143
|
+
help="Output in JSON format for scripting",
|
|
144
|
+
)
|
|
145
|
+
def doctor(output_json: bool) -> None:
|
|
146
|
+
"""
|
|
147
|
+
Check Recce Cloud setup and configuration.
|
|
148
|
+
|
|
149
|
+
Validates login status, project binding, and session availability.
|
|
150
|
+
Provides actionable suggestions when issues are found.
|
|
151
|
+
|
|
152
|
+
\b
|
|
153
|
+
Examples:
|
|
154
|
+
# Check setup status
|
|
155
|
+
recce-cloud doctor
|
|
156
|
+
|
|
157
|
+
# Machine-readable output
|
|
158
|
+
recce-cloud doctor --json
|
|
159
|
+
"""
|
|
160
|
+
console = Console()
|
|
161
|
+
|
|
162
|
+
# Run diagnostic checks
|
|
163
|
+
service = DiagnosticService()
|
|
164
|
+
results = service.run_all_checks()
|
|
165
|
+
|
|
166
|
+
# Output results
|
|
167
|
+
if output_json:
|
|
168
|
+
console.print(json.dumps(results.to_dict(), indent=2, default=str))
|
|
169
|
+
else:
|
|
170
|
+
renderer = DiagnosticRenderer(console)
|
|
171
|
+
renderer.render_all(results)
|
|
172
|
+
|
|
173
|
+
# Exit with appropriate code
|
|
174
|
+
sys.exit(0 if results.all_passed else 1)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration module for Recce Cloud CLI.
|
|
3
|
+
|
|
4
|
+
This module provides project binding and configuration resolution.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from recce_cloud.config.project_config import (
|
|
8
|
+
clear_project_binding,
|
|
9
|
+
get_project_binding,
|
|
10
|
+
save_project_binding,
|
|
11
|
+
)
|
|
12
|
+
from recce_cloud.config.resolver import resolve_config
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"clear_project_binding",
|
|
16
|
+
"get_project_binding",
|
|
17
|
+
"resolve_config",
|
|
18
|
+
"save_project_binding",
|
|
19
|
+
]
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project configuration management for .recce/config.
|
|
3
|
+
|
|
4
|
+
This module manages local project binding stored in the project directory.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Dict, Optional
|
|
10
|
+
|
|
11
|
+
import yaml
|
|
12
|
+
|
|
13
|
+
RECCE_CONFIG_DIR = ".recce"
|
|
14
|
+
RECCE_CONFIG_FILE = "config"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_config_path(project_dir: Optional[str] = None) -> Path:
|
|
18
|
+
"""
|
|
19
|
+
Get the path to the local config file.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
project_dir: Project directory path. Defaults to current directory.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Path to .recce/config file.
|
|
26
|
+
"""
|
|
27
|
+
base_dir = Path(project_dir) if project_dir else Path.cwd()
|
|
28
|
+
return base_dir / RECCE_CONFIG_DIR / RECCE_CONFIG_FILE
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_config_dir(project_dir: Optional[str] = None) -> Path:
|
|
32
|
+
"""
|
|
33
|
+
Get the path to the .recce directory.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
project_dir: Project directory path. Defaults to current directory.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Path to .recce directory.
|
|
40
|
+
"""
|
|
41
|
+
base_dir = Path(project_dir) if project_dir else Path.cwd()
|
|
42
|
+
return base_dir / RECCE_CONFIG_DIR
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def load_config(project_dir: Optional[str] = None) -> Dict[str, Any]:
|
|
46
|
+
"""
|
|
47
|
+
Load configuration from .recce/config.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
project_dir: Project directory path. Defaults to current directory.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Configuration dictionary, or empty dict if file doesn't exist.
|
|
54
|
+
"""
|
|
55
|
+
config_path = get_config_path(project_dir)
|
|
56
|
+
if not config_path.exists():
|
|
57
|
+
return {}
|
|
58
|
+
|
|
59
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
60
|
+
config = yaml.safe_load(f)
|
|
61
|
+
return config if config else {}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def save_config(config: Dict[str, Any], project_dir: Optional[str] = None) -> None:
|
|
65
|
+
"""
|
|
66
|
+
Save configuration to .recce/config.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
config: Configuration dictionary to save.
|
|
70
|
+
project_dir: Project directory path. Defaults to current directory.
|
|
71
|
+
"""
|
|
72
|
+
config_dir = get_config_dir(project_dir)
|
|
73
|
+
config_path = get_config_path(project_dir)
|
|
74
|
+
|
|
75
|
+
# Ensure .recce directory exists
|
|
76
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
77
|
+
|
|
78
|
+
with open(config_path, "w", encoding="utf-8") as f:
|
|
79
|
+
yaml.dump(config, f, default_flow_style=False, sort_keys=False)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def get_project_binding(project_dir: Optional[str] = None) -> Optional[Dict[str, Any]]:
|
|
83
|
+
"""
|
|
84
|
+
Get the current project binding from .recce/config.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
project_dir: Project directory path. Defaults to current directory.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Dictionary with org, project, bound_at, bound_by fields,
|
|
91
|
+
or None if not bound.
|
|
92
|
+
"""
|
|
93
|
+
config = load_config(project_dir)
|
|
94
|
+
cloud_config = config.get("cloud", {})
|
|
95
|
+
|
|
96
|
+
if not cloud_config.get("org") or not cloud_config.get("project"):
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
"org": cloud_config.get("org"),
|
|
101
|
+
"project": cloud_config.get("project"),
|
|
102
|
+
"bound_at": cloud_config.get("bound_at"),
|
|
103
|
+
"bound_by": cloud_config.get("bound_by"),
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def save_project_binding(
|
|
108
|
+
org: str,
|
|
109
|
+
project: str,
|
|
110
|
+
bound_by: Optional[str] = None,
|
|
111
|
+
project_dir: Optional[str] = None,
|
|
112
|
+
) -> None:
|
|
113
|
+
"""
|
|
114
|
+
Save project binding to .recce/config.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
org: Organization name/slug.
|
|
118
|
+
project: Project name/slug.
|
|
119
|
+
bound_by: Email of user who created the binding.
|
|
120
|
+
project_dir: Project directory path. Defaults to current directory.
|
|
121
|
+
"""
|
|
122
|
+
config = load_config(project_dir)
|
|
123
|
+
|
|
124
|
+
config["version"] = 1
|
|
125
|
+
config["cloud"] = {
|
|
126
|
+
"org": org,
|
|
127
|
+
"project": project,
|
|
128
|
+
"bound_at": datetime.now(timezone.utc).isoformat(),
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if bound_by:
|
|
132
|
+
config["cloud"]["bound_by"] = bound_by
|
|
133
|
+
|
|
134
|
+
save_config(config, project_dir)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def clear_project_binding(project_dir: Optional[str] = None) -> bool:
|
|
138
|
+
"""
|
|
139
|
+
Clear project binding from .recce/config.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
project_dir: Project directory path. Defaults to current directory.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
True if binding was cleared, False if no binding existed.
|
|
146
|
+
"""
|
|
147
|
+
config = load_config(project_dir)
|
|
148
|
+
|
|
149
|
+
if "cloud" not in config:
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
del config["cloud"]
|
|
153
|
+
save_config(config, project_dir)
|
|
154
|
+
return True
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def add_to_gitignore(project_dir: Optional[str] = None) -> bool:
|
|
158
|
+
"""
|
|
159
|
+
Add .recce/ to .gitignore if not already present.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
project_dir: Project directory path. Defaults to current directory.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
True if .gitignore was modified, False otherwise.
|
|
166
|
+
"""
|
|
167
|
+
base_dir = Path(project_dir) if project_dir else Path.cwd()
|
|
168
|
+
gitignore_path = base_dir / ".gitignore"
|
|
169
|
+
|
|
170
|
+
# Check if .recce/ is already in .gitignore
|
|
171
|
+
if gitignore_path.exists():
|
|
172
|
+
with open(gitignore_path, "r", encoding="utf-8") as f:
|
|
173
|
+
content = f.read()
|
|
174
|
+
if ".recce/" in content or ".recce" in content:
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
# Append .recce/ to .gitignore
|
|
178
|
+
with open(gitignore_path, "a", encoding="utf-8") as f:
|
|
179
|
+
if gitignore_path.exists():
|
|
180
|
+
# Check if file ends with newline
|
|
181
|
+
with open(gitignore_path, "r", encoding="utf-8") as rf:
|
|
182
|
+
content = rf.read()
|
|
183
|
+
if content and not content.endswith("\n"):
|
|
184
|
+
f.write("\n")
|
|
185
|
+
f.write("\n# Recce Cloud local config\n.recce/\n")
|
|
186
|
+
|
|
187
|
+
return True
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration resolver for Recce Cloud CLI.
|
|
3
|
+
|
|
4
|
+
Resolves org/project configuration from multiple sources with priority:
|
|
5
|
+
1. CLI flags (--org, --project)
|
|
6
|
+
2. Environment variables (RECCE_ORG, RECCE_PROJECT)
|
|
7
|
+
3. Local config file (.recce/config)
|
|
8
|
+
4. Error (no configuration found)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
from recce_cloud.config.project_config import get_project_binding
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class ResolvedConfig:
|
|
20
|
+
"""Resolved configuration with source information."""
|
|
21
|
+
|
|
22
|
+
org: str
|
|
23
|
+
project: str
|
|
24
|
+
source: str # "cli", "env", "config"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ConfigurationError(Exception):
|
|
28
|
+
"""Raised when configuration cannot be resolved."""
|
|
29
|
+
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def resolve_config(
|
|
34
|
+
cli_org: Optional[str] = None,
|
|
35
|
+
cli_project: Optional[str] = None,
|
|
36
|
+
project_dir: Optional[str] = None,
|
|
37
|
+
) -> ResolvedConfig:
|
|
38
|
+
"""
|
|
39
|
+
Resolve org/project configuration from multiple sources.
|
|
40
|
+
|
|
41
|
+
Priority order:
|
|
42
|
+
1. CLI flags (--org, --project) - highest priority
|
|
43
|
+
2. Environment variables (RECCE_ORG, RECCE_PROJECT)
|
|
44
|
+
3. Local config file (.recce/config)
|
|
45
|
+
4. Error if nothing found
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
cli_org: Organization from CLI flag.
|
|
49
|
+
cli_project: Project from CLI flag.
|
|
50
|
+
project_dir: Project directory for local config lookup.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
ResolvedConfig with org, project, and source.
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
ConfigurationError: If org/project cannot be resolved.
|
|
57
|
+
"""
|
|
58
|
+
# Priority 1: CLI flags
|
|
59
|
+
if cli_org and cli_project:
|
|
60
|
+
return ResolvedConfig(org=cli_org, project=cli_project, source="cli")
|
|
61
|
+
|
|
62
|
+
# Priority 2: Environment variables
|
|
63
|
+
env_org = os.environ.get("RECCE_ORG")
|
|
64
|
+
env_project = os.environ.get("RECCE_PROJECT")
|
|
65
|
+
if env_org and env_project:
|
|
66
|
+
return ResolvedConfig(org=env_org, project=env_project, source="env")
|
|
67
|
+
|
|
68
|
+
# Priority 3: Local config file
|
|
69
|
+
binding = get_project_binding(project_dir)
|
|
70
|
+
if binding:
|
|
71
|
+
return ResolvedConfig(
|
|
72
|
+
org=binding["org"],
|
|
73
|
+
project=binding["project"],
|
|
74
|
+
source="config",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Priority 4: Error
|
|
78
|
+
raise ConfigurationError(
|
|
79
|
+
"No project configured. Run 'recce-cloud init' to bind this directory to a project, "
|
|
80
|
+
"or use --org and --project flags."
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def resolve_org(
|
|
85
|
+
cli_org: Optional[str] = None,
|
|
86
|
+
project_dir: Optional[str] = None,
|
|
87
|
+
) -> Optional[str]:
|
|
88
|
+
"""
|
|
89
|
+
Resolve organization from multiple sources.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
cli_org: Organization from CLI flag.
|
|
93
|
+
project_dir: Project directory for local config lookup.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Organization name/slug, or None if not found.
|
|
97
|
+
"""
|
|
98
|
+
if cli_org:
|
|
99
|
+
return cli_org
|
|
100
|
+
|
|
101
|
+
env_org = os.environ.get("RECCE_ORG")
|
|
102
|
+
if env_org:
|
|
103
|
+
return env_org
|
|
104
|
+
|
|
105
|
+
binding = get_project_binding(project_dir)
|
|
106
|
+
if binding:
|
|
107
|
+
return binding["org"]
|
|
108
|
+
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def resolve_project(
|
|
113
|
+
cli_project: Optional[str] = None,
|
|
114
|
+
project_dir: Optional[str] = None,
|
|
115
|
+
) -> Optional[str]:
|
|
116
|
+
"""
|
|
117
|
+
Resolve project from multiple sources.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
cli_project: Project from CLI flag.
|
|
121
|
+
project_dir: Project directory for local config lookup.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Project name/slug, or None if not found.
|
|
125
|
+
"""
|
|
126
|
+
if cli_project:
|
|
127
|
+
return cli_project
|
|
128
|
+
|
|
129
|
+
env_project = os.environ.get("RECCE_PROJECT")
|
|
130
|
+
if env_project:
|
|
131
|
+
return env_project
|
|
132
|
+
|
|
133
|
+
binding = get_project_binding(project_dir)
|
|
134
|
+
if binding:
|
|
135
|
+
return binding["project"]
|
|
136
|
+
|
|
137
|
+
return None
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Services layer for business logic
|