codeframe-ai 0.9.0__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.
- codeframe/__init__.py +11 -0
- codeframe/__main__.py +20 -0
- codeframe/adapters/__init__.py +5 -0
- codeframe/adapters/e2b/__init__.py +13 -0
- codeframe/adapters/e2b/adapter.py +342 -0
- codeframe/adapters/e2b/budget.py +71 -0
- codeframe/adapters/e2b/credential_scanner.py +134 -0
- codeframe/adapters/llm/__init__.py +92 -0
- codeframe/adapters/llm/anthropic.py +414 -0
- codeframe/adapters/llm/base.py +444 -0
- codeframe/adapters/llm/mock.py +281 -0
- codeframe/adapters/llm/openai.py +483 -0
- codeframe/agents/__init__.py +8 -0
- codeframe/agents/dependency_resolver.py +714 -0
- codeframe/auth/__init__.py +16 -0
- codeframe/auth/api_key_router.py +238 -0
- codeframe/auth/api_keys.py +156 -0
- codeframe/auth/dependencies.py +358 -0
- codeframe/auth/manager.py +178 -0
- codeframe/auth/models.py +30 -0
- codeframe/auth/router.py +93 -0
- codeframe/auth/schemas.py +15 -0
- codeframe/auth/scopes.py +53 -0
- codeframe/cli/__init__.py +12 -0
- codeframe/cli/__main__.py +20 -0
- codeframe/cli/api_client.py +275 -0
- codeframe/cli/app.py +5688 -0
- codeframe/cli/auth.py +122 -0
- codeframe/cli/auth_commands.py +958 -0
- codeframe/cli/commands/__init__.py +5 -0
- codeframe/cli/config_commands.py +79 -0
- codeframe/cli/dashboard_commands.py +67 -0
- codeframe/cli/engines_commands.py +205 -0
- codeframe/cli/env_commands.py +409 -0
- codeframe/cli/helpers.py +56 -0
- codeframe/cli/hooks_commands.py +208 -0
- codeframe/cli/import_commands.py +129 -0
- codeframe/cli/pr_commands.py +549 -0
- codeframe/cli/proof_commands.py +415 -0
- codeframe/cli/stats_commands.py +311 -0
- codeframe/cli/telemetry_runtime.py +153 -0
- codeframe/cli/validators.py +123 -0
- codeframe/config/rate_limits.py +165 -0
- codeframe/core/__init__.py +15 -0
- codeframe/core/adapters/__init__.py +43 -0
- codeframe/core/adapters/agent_adapter.py +114 -0
- codeframe/core/adapters/builtin.py +326 -0
- codeframe/core/adapters/claude_code.py +62 -0
- codeframe/core/adapters/codex.py +393 -0
- codeframe/core/adapters/git_utils.py +40 -0
- codeframe/core/adapters/kilocode.py +126 -0
- codeframe/core/adapters/opencode.py +48 -0
- codeframe/core/adapters/streaming_chat.py +483 -0
- codeframe/core/adapters/subprocess_adapter.py +213 -0
- codeframe/core/adapters/verification_wrapper.py +269 -0
- codeframe/core/agent.py +2183 -0
- codeframe/core/agents_config.py +569 -0
- codeframe/core/api_key_service.py +211 -0
- codeframe/core/artifacts.py +428 -0
- codeframe/core/blocker_detection.py +218 -0
- codeframe/core/blockers.py +433 -0
- codeframe/core/checkpoints.py +481 -0
- codeframe/core/conductor.py +2255 -0
- codeframe/core/config.py +827 -0
- codeframe/core/config_watcher.py +268 -0
- codeframe/core/context.py +542 -0
- codeframe/core/context_packager.py +234 -0
- codeframe/core/credentials.py +735 -0
- codeframe/core/dependency_analyzer.py +229 -0
- codeframe/core/dependency_graph.py +290 -0
- codeframe/core/diagnostic_agent.py +712 -0
- codeframe/core/diagnostics.py +616 -0
- codeframe/core/editor.py +556 -0
- codeframe/core/engine_registry.py +256 -0
- codeframe/core/engine_stats.py +231 -0
- codeframe/core/environment.py +697 -0
- codeframe/core/events.py +375 -0
- codeframe/core/executor.py +1005 -0
- codeframe/core/fix_tracker.py +480 -0
- codeframe/core/gates.py +1322 -0
- codeframe/core/git.py +477 -0
- codeframe/core/github_connect_service.py +178 -0
- codeframe/core/github_integration_config.py +118 -0
- codeframe/core/github_issues_service.py +449 -0
- codeframe/core/hooks.py +184 -0
- codeframe/core/importers/__init__.py +1 -0
- codeframe/core/importers/ralph.py +540 -0
- codeframe/core/installer.py +650 -0
- codeframe/core/models.py +1026 -0
- codeframe/core/notifications_config.py +183 -0
- codeframe/core/planner.py +437 -0
- codeframe/core/prd.py +670 -0
- codeframe/core/prd_discovery.py +1118 -0
- codeframe/core/prd_stress_test.py +499 -0
- codeframe/core/progress.py +126 -0
- codeframe/core/proof/__init__.py +34 -0
- codeframe/core/proof/capture.py +79 -0
- codeframe/core/proof/evidence.py +56 -0
- codeframe/core/proof/ledger.py +574 -0
- codeframe/core/proof/models.py +162 -0
- codeframe/core/proof/obligations.py +103 -0
- codeframe/core/proof/runner.py +233 -0
- codeframe/core/proof/scope.py +81 -0
- codeframe/core/proof/stubs.py +156 -0
- codeframe/core/quick_fixes.py +558 -0
- codeframe/core/react_agent.py +1650 -0
- codeframe/core/reconciliation.py +183 -0
- codeframe/core/replay.py +788 -0
- codeframe/core/review.py +285 -0
- codeframe/core/runtime.py +1134 -0
- codeframe/core/sandbox/__init__.py +27 -0
- codeframe/core/sandbox/context.py +98 -0
- codeframe/core/sandbox/worktree.py +20 -0
- codeframe/core/schedule.py +396 -0
- codeframe/core/stall_detector.py +71 -0
- codeframe/core/stall_monitor.py +134 -0
- codeframe/core/state_machine.py +121 -0
- codeframe/core/streaming.py +502 -0
- codeframe/core/task_tree.py +400 -0
- codeframe/core/tasks.py +1022 -0
- codeframe/core/telemetry.py +232 -0
- codeframe/core/templates.py +221 -0
- codeframe/core/tools.py +942 -0
- codeframe/core/workspace.py +887 -0
- codeframe/core/worktrees.py +276 -0
- codeframe/git/__init__.py +5 -0
- codeframe/git/github_integration.py +505 -0
- codeframe/lib/__init__.py +0 -0
- codeframe/lib/audit_logger.py +248 -0
- codeframe/lib/metrics_tracker.py +800 -0
- codeframe/lib/quality/__init__.py +7 -0
- codeframe/lib/quality/complexity_analyzer.py +316 -0
- codeframe/lib/quality/owasp_patterns.py +284 -0
- codeframe/lib/quality/security_scanner.py +250 -0
- codeframe/lib/rate_limiter.py +312 -0
- codeframe/notifications/__init__.py +0 -0
- codeframe/notifications/webhook.py +380 -0
- codeframe/planning/__init__.py +30 -0
- codeframe/planning/issue_generator.py +219 -0
- codeframe/planning/prd_template_functions.py +137 -0
- codeframe/planning/prd_templates.py +975 -0
- codeframe/planning/task_scheduler.py +511 -0
- codeframe/planning/task_templates.py +533 -0
- codeframe/platform_store/__init__.py +5 -0
- codeframe/platform_store/database.py +277 -0
- codeframe/platform_store/repositories/__init__.py +24 -0
- codeframe/platform_store/repositories/api_key_repository.py +245 -0
- codeframe/platform_store/repositories/audit_repository.py +67 -0
- codeframe/platform_store/repositories/base.py +295 -0
- codeframe/platform_store/repositories/interactive_sessions.py +165 -0
- codeframe/platform_store/repositories/token_repository.py +598 -0
- codeframe/platform_store/repositories/workspace_registry_repository.py +175 -0
- codeframe/platform_store/schema_manager.py +321 -0
- codeframe/templates/AGENTS.md.default +94 -0
- codeframe/tui/__init__.py +5 -0
- codeframe/tui/app.py +256 -0
- codeframe/tui/data_service.py +103 -0
- codeframe/ui/__init__.py +0 -0
- codeframe/ui/dependencies.py +103 -0
- codeframe/ui/models.py +999 -0
- codeframe/ui/response_models.py +201 -0
- codeframe/ui/routers/__init__.py +5 -0
- codeframe/ui/routers/_helpers.py +29 -0
- codeframe/ui/routers/batches_v2.py +315 -0
- codeframe/ui/routers/blockers_v2.py +320 -0
- codeframe/ui/routers/checkpoints_v2.py +310 -0
- codeframe/ui/routers/costs_v2.py +322 -0
- codeframe/ui/routers/diagnose_v2.py +225 -0
- codeframe/ui/routers/discovery_v2.py +417 -0
- codeframe/ui/routers/environment_v2.py +284 -0
- codeframe/ui/routers/events_v2.py +75 -0
- codeframe/ui/routers/gates_v2.py +166 -0
- codeframe/ui/routers/git_v2.py +284 -0
- codeframe/ui/routers/github_integrations_v2.py +532 -0
- codeframe/ui/routers/interactive_sessions_v2.py +238 -0
- codeframe/ui/routers/pr_v2.py +709 -0
- codeframe/ui/routers/prd_v2.py +695 -0
- codeframe/ui/routers/proof_v2.py +755 -0
- codeframe/ui/routers/review_v2.py +360 -0
- codeframe/ui/routers/schedule_v2.py +214 -0
- codeframe/ui/routers/session_chat_ws.py +354 -0
- codeframe/ui/routers/settings_v2.py +562 -0
- codeframe/ui/routers/streaming_v2.py +155 -0
- codeframe/ui/routers/tasks_v2.py +1098 -0
- codeframe/ui/routers/templates_v2.py +232 -0
- codeframe/ui/routers/terminal_ws.py +267 -0
- codeframe/ui/routers/workspace_v2.py +527 -0
- codeframe/ui/server.py +568 -0
- codeframe/ui/shared.py +241 -0
- codeframe/workspace/__init__.py +5 -0
- codeframe/workspace/manager.py +249 -0
- codeframe_ai-0.9.0.dist-info/METADATA +517 -0
- codeframe_ai-0.9.0.dist-info/RECORD +197 -0
- codeframe_ai-0.9.0.dist-info/WHEEL +5 -0
- codeframe_ai-0.9.0.dist-info/entry_points.txt +3 -0
- codeframe_ai-0.9.0.dist-info/licenses/LICENSE +661 -0
- codeframe_ai-0.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,958 @@
|
|
|
1
|
+
"""CLI authentication commands (login, logout, register, whoami, credentials, API keys).
|
|
2
|
+
|
|
3
|
+
This module provides commands for:
|
|
4
|
+
- Logging in with email/password
|
|
5
|
+
- Logging out (clearing credentials)
|
|
6
|
+
- Registering a new account
|
|
7
|
+
- Viewing current user info
|
|
8
|
+
- Managing API credentials (setup, list, validate, rotate, remove)
|
|
9
|
+
- Managing API keys (create, list, revoke, rotate)
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
codeframe auth login --email user@example.com --password secret
|
|
13
|
+
codeframe auth logout
|
|
14
|
+
codeframe auth register --email new@example.com --password secret
|
|
15
|
+
codeframe auth whoami
|
|
16
|
+
codeframe auth setup --provider anthropic --value sk-ant-...
|
|
17
|
+
codeframe auth list
|
|
18
|
+
codeframe auth validate anthropic
|
|
19
|
+
codeframe auth rotate anthropic --value sk-ant-new-...
|
|
20
|
+
codeframe auth remove anthropic --yes
|
|
21
|
+
codeframe auth api-key-create --name "My Key" --user-id 1
|
|
22
|
+
codeframe auth api-key-list --user-id 1
|
|
23
|
+
codeframe auth api-key-revoke <key-id> --user-id 1 --yes
|
|
24
|
+
codeframe auth api-key-rotate <key-id> --user-id 1
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import logging
|
|
28
|
+
import os
|
|
29
|
+
from typing import Optional, Tuple
|
|
30
|
+
|
|
31
|
+
import requests
|
|
32
|
+
import typer
|
|
33
|
+
from rich.table import Table
|
|
34
|
+
|
|
35
|
+
from codeframe.cli.auth import store_token, clear_token, is_authenticated
|
|
36
|
+
from codeframe.cli.api_client import APIClient, AuthenticationError, get_api_base_url
|
|
37
|
+
from codeframe.cli.helpers import console
|
|
38
|
+
from codeframe.core.credentials import (
|
|
39
|
+
CredentialManager,
|
|
40
|
+
CredentialProvider,
|
|
41
|
+
CredentialSource,
|
|
42
|
+
)
|
|
43
|
+
from codeframe.core.api_key_service import ApiKeyService
|
|
44
|
+
from codeframe.platform_store.database import Database
|
|
45
|
+
|
|
46
|
+
logger = logging.getLogger(__name__)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# =============================================================================
|
|
50
|
+
# Database Helper for CLI
|
|
51
|
+
# =============================================================================
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_db_for_cli() -> Database:
|
|
55
|
+
"""Get database instance for CLI commands.
|
|
56
|
+
|
|
57
|
+
Uses DATABASE_PATH environment variable if set, otherwise defaults
|
|
58
|
+
to .codeframe/state.db in the current directory.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Initialized Database instance
|
|
62
|
+
"""
|
|
63
|
+
db_path = os.getenv(
|
|
64
|
+
"DATABASE_PATH",
|
|
65
|
+
os.path.join(os.getcwd(), ".codeframe", "state.db")
|
|
66
|
+
)
|
|
67
|
+
db = Database(db_path)
|
|
68
|
+
db.initialize()
|
|
69
|
+
return db
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# Provider name aliases for CLI convenience
|
|
73
|
+
PROVIDER_ALIASES = {
|
|
74
|
+
# Anthropic
|
|
75
|
+
"anthropic": CredentialProvider.LLM_ANTHROPIC,
|
|
76
|
+
"claude": CredentialProvider.LLM_ANTHROPIC,
|
|
77
|
+
"llm_anthropic": CredentialProvider.LLM_ANTHROPIC,
|
|
78
|
+
# OpenAI
|
|
79
|
+
"openai": CredentialProvider.LLM_OPENAI,
|
|
80
|
+
"gpt": CredentialProvider.LLM_OPENAI,
|
|
81
|
+
"gpt4": CredentialProvider.LLM_OPENAI,
|
|
82
|
+
"llm_openai": CredentialProvider.LLM_OPENAI,
|
|
83
|
+
# GitHub
|
|
84
|
+
"github": CredentialProvider.GIT_GITHUB,
|
|
85
|
+
"gh": CredentialProvider.GIT_GITHUB,
|
|
86
|
+
"git_github": CredentialProvider.GIT_GITHUB,
|
|
87
|
+
# GitLab
|
|
88
|
+
"gitlab": CredentialProvider.GIT_GITLAB,
|
|
89
|
+
"gl": CredentialProvider.GIT_GITLAB,
|
|
90
|
+
"git_gitlab": CredentialProvider.GIT_GITLAB,
|
|
91
|
+
# CI/CD
|
|
92
|
+
"cicd": CredentialProvider.CICD_GENERIC,
|
|
93
|
+
"ci": CredentialProvider.CICD_GENERIC,
|
|
94
|
+
"cicd_generic": CredentialProvider.CICD_GENERIC,
|
|
95
|
+
# Database
|
|
96
|
+
"database": CredentialProvider.DATABASE,
|
|
97
|
+
"db": CredentialProvider.DATABASE,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def resolve_provider_name(name: str) -> CredentialProvider:
|
|
102
|
+
"""Resolve a provider name/alias to CredentialProvider enum.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
name: Provider name or alias (case-insensitive)
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
CredentialProvider enum value
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
ValueError: If name doesn't match any known provider
|
|
112
|
+
"""
|
|
113
|
+
normalized = name.lower().strip()
|
|
114
|
+
|
|
115
|
+
if normalized in PROVIDER_ALIASES:
|
|
116
|
+
return PROVIDER_ALIASES[normalized]
|
|
117
|
+
|
|
118
|
+
# Try direct enum lookup
|
|
119
|
+
try:
|
|
120
|
+
return CredentialProvider[name.upper()]
|
|
121
|
+
except KeyError:
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
valid_names = sorted(set(PROVIDER_ALIASES.keys()))
|
|
125
|
+
raise ValueError(
|
|
126
|
+
f"Unknown provider: '{name}'. Valid providers: {', '.join(valid_names)}"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def validate_anthropic_credential(api_key: str) -> Tuple[bool, str]:
|
|
131
|
+
"""Validate Anthropic API key by making a test request.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
api_key: The API key to validate
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Tuple of (is_valid, message)
|
|
138
|
+
"""
|
|
139
|
+
try:
|
|
140
|
+
from anthropic import Anthropic, AuthenticationError as AnthropicAuthError
|
|
141
|
+
from anthropic import APIConnectionError, RateLimitError, APIStatusError
|
|
142
|
+
|
|
143
|
+
client = Anthropic(api_key=api_key)
|
|
144
|
+
# Make a minimal request to validate
|
|
145
|
+
client.messages.create(
|
|
146
|
+
model="claude-3-haiku-20240307",
|
|
147
|
+
max_tokens=1,
|
|
148
|
+
messages=[{"role": "user", "content": "hi"}],
|
|
149
|
+
)
|
|
150
|
+
return True, "API key is valid"
|
|
151
|
+
except AnthropicAuthError:
|
|
152
|
+
return False, "Invalid API key - authentication failed"
|
|
153
|
+
except APIConnectionError as e:
|
|
154
|
+
return False, f"Unable to validate - network error: {e}"
|
|
155
|
+
except RateLimitError:
|
|
156
|
+
# Rate limited but key is valid (they wouldn't rate limit invalid keys)
|
|
157
|
+
return True, "API key appears valid (rate limited)"
|
|
158
|
+
except APIStatusError as e:
|
|
159
|
+
return False, f"Unable to validate - API error (HTTP {e.status_code})"
|
|
160
|
+
except ImportError:
|
|
161
|
+
return False, "Unable to validate - anthropic package not installed"
|
|
162
|
+
except Exception as e:
|
|
163
|
+
return False, f"Unable to validate - unexpected error: {type(e).__name__}: {e}"
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def validate_openai_credential(api_key: str) -> Tuple[bool, str]:
|
|
167
|
+
"""Validate OpenAI API key by making a test request.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
api_key: The API key to validate
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Tuple of (is_valid, message)
|
|
174
|
+
"""
|
|
175
|
+
try:
|
|
176
|
+
from openai import OpenAI, AuthenticationError as OpenAIAuthError
|
|
177
|
+
from openai import APIConnectionError, RateLimitError, APIStatusError
|
|
178
|
+
|
|
179
|
+
client = OpenAI(api_key=api_key)
|
|
180
|
+
# List models is a cheap validation
|
|
181
|
+
client.models.list()
|
|
182
|
+
return True, "API key is valid"
|
|
183
|
+
except OpenAIAuthError:
|
|
184
|
+
return False, "Invalid API key - authentication failed"
|
|
185
|
+
except APIConnectionError as e:
|
|
186
|
+
return False, f"Unable to validate - network error: {e}"
|
|
187
|
+
except RateLimitError:
|
|
188
|
+
# Rate limited but key is valid
|
|
189
|
+
return True, "API key appears valid (rate limited)"
|
|
190
|
+
except APIStatusError as e:
|
|
191
|
+
return False, f"Unable to validate - API error (HTTP {e.status_code})"
|
|
192
|
+
except ImportError:
|
|
193
|
+
return False, "Unable to validate - openai package not installed"
|
|
194
|
+
except Exception as e:
|
|
195
|
+
return False, f"Unable to validate - unexpected error: {type(e).__name__}: {e}"
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def validate_github_credential(token: str) -> Tuple[bool, str]:
|
|
199
|
+
"""Validate GitHub token by making a test request.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
token: The GitHub token to validate
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Tuple of (is_valid, message)
|
|
206
|
+
"""
|
|
207
|
+
try:
|
|
208
|
+
response = requests.get(
|
|
209
|
+
"https://api.github.com/user",
|
|
210
|
+
headers={
|
|
211
|
+
"Authorization": f"Bearer {token}",
|
|
212
|
+
"Accept": "application/vnd.github.v3+json",
|
|
213
|
+
},
|
|
214
|
+
timeout=10,
|
|
215
|
+
)
|
|
216
|
+
if response.status_code == 200:
|
|
217
|
+
user = response.json()
|
|
218
|
+
return True, f"Token valid for user: {user.get('login', 'unknown')}"
|
|
219
|
+
elif response.status_code == 401:
|
|
220
|
+
return False, "Invalid token - authentication failed"
|
|
221
|
+
elif response.status_code == 403:
|
|
222
|
+
# Forbidden - token may be valid but rate limited or lacking permissions
|
|
223
|
+
return False, "Token rejected - rate limited or insufficient permissions"
|
|
224
|
+
elif response.status_code == 429:
|
|
225
|
+
# Rate limited but token format was accepted
|
|
226
|
+
return True, "Token appears valid (rate limited)"
|
|
227
|
+
else:
|
|
228
|
+
return False, f"Unable to validate - GitHub API returned HTTP {response.status_code}"
|
|
229
|
+
except requests.ConnectionError:
|
|
230
|
+
return False, "Unable to validate - network error (cannot reach GitHub)"
|
|
231
|
+
except requests.Timeout:
|
|
232
|
+
return False, "Unable to validate - request timed out"
|
|
233
|
+
except requests.RequestException as e:
|
|
234
|
+
return False, f"Unable to validate - request error: {e}"
|
|
235
|
+
except Exception as e:
|
|
236
|
+
return False, f"Unable to validate - unexpected error: {type(e).__name__}: {e}"
|
|
237
|
+
|
|
238
|
+
auth_app = typer.Typer(
|
|
239
|
+
name="auth",
|
|
240
|
+
help="Authentication commands",
|
|
241
|
+
no_args_is_help=True,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@auth_app.command()
|
|
246
|
+
def login(
|
|
247
|
+
email: Optional[str] = typer.Option(None, "--email", "-e", help="Your email address"),
|
|
248
|
+
password: Optional[str] = typer.Option(None, "--password", "-p", help="Your password", hide_input=True),
|
|
249
|
+
):
|
|
250
|
+
"""Log in to CodeFRAME.
|
|
251
|
+
|
|
252
|
+
Authenticates with the server and stores your JWT token locally.
|
|
253
|
+
You will be prompted for email and password if not provided.
|
|
254
|
+
|
|
255
|
+
Examples:
|
|
256
|
+
|
|
257
|
+
codeframe auth login
|
|
258
|
+
|
|
259
|
+
codeframe auth login --email user@example.com
|
|
260
|
+
|
|
261
|
+
codeframe auth login -e user@example.com -p secret
|
|
262
|
+
"""
|
|
263
|
+
# Prompt for email if not provided
|
|
264
|
+
if not email:
|
|
265
|
+
email = typer.prompt("Email")
|
|
266
|
+
|
|
267
|
+
# Prompt for password if not provided
|
|
268
|
+
if not password:
|
|
269
|
+
password = typer.prompt("Password", hide_input=True)
|
|
270
|
+
|
|
271
|
+
# Call login API
|
|
272
|
+
base_url = get_api_base_url()
|
|
273
|
+
login_url = f"{base_url}/auth/jwt/login"
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
# FastAPI Users expects form data, not JSON
|
|
277
|
+
response = requests.post(
|
|
278
|
+
login_url,
|
|
279
|
+
data={
|
|
280
|
+
"username": email, # FastAPI Users uses 'username' field
|
|
281
|
+
"password": password,
|
|
282
|
+
},
|
|
283
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
284
|
+
timeout=30,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
if response.status_code == 200:
|
|
288
|
+
data = response.json()
|
|
289
|
+
token = data.get("access_token")
|
|
290
|
+
|
|
291
|
+
if token:
|
|
292
|
+
store_token(token)
|
|
293
|
+
console.print(f"[green]✓ Successfully logged in as {email}[/green]")
|
|
294
|
+
else:
|
|
295
|
+
console.print("[red]Error:[/red] No token received from server")
|
|
296
|
+
raise typer.Exit(1)
|
|
297
|
+
|
|
298
|
+
elif response.status_code == 400:
|
|
299
|
+
# Bad credentials
|
|
300
|
+
console.print("[red]Error:[/red] Invalid email or password")
|
|
301
|
+
raise typer.Exit(1)
|
|
302
|
+
|
|
303
|
+
elif response.status_code == 422:
|
|
304
|
+
# Validation error
|
|
305
|
+
console.print("[red]Error:[/red] Invalid request format")
|
|
306
|
+
raise typer.Exit(1)
|
|
307
|
+
|
|
308
|
+
else:
|
|
309
|
+
console.print(f"[red]Error:[/red] Login failed (HTTP {response.status_code})")
|
|
310
|
+
raise typer.Exit(1)
|
|
311
|
+
|
|
312
|
+
except requests.ConnectionError:
|
|
313
|
+
console.print(f"[red]Error:[/red] Cannot connect to server at {base_url}")
|
|
314
|
+
console.print("Make sure the CodeFRAME server is running: codeframe serve")
|
|
315
|
+
raise typer.Exit(1)
|
|
316
|
+
|
|
317
|
+
except requests.Timeout:
|
|
318
|
+
console.print("[red]Error:[/red] Request timed out")
|
|
319
|
+
raise typer.Exit(1)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@auth_app.command()
|
|
323
|
+
def logout():
|
|
324
|
+
"""Log out of CodeFRAME.
|
|
325
|
+
|
|
326
|
+
Removes your stored credentials from the local machine.
|
|
327
|
+
|
|
328
|
+
Example:
|
|
329
|
+
|
|
330
|
+
codeframe auth logout
|
|
331
|
+
"""
|
|
332
|
+
clear_token()
|
|
333
|
+
console.print("[green]✓ Logged out successfully[/green]")
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
@auth_app.command()
|
|
337
|
+
def register(
|
|
338
|
+
email: Optional[str] = typer.Option(None, "--email", "-e", help="Your email address"),
|
|
339
|
+
password: Optional[str] = typer.Option(None, "--password", "-p", help="Your password", hide_input=True),
|
|
340
|
+
):
|
|
341
|
+
"""Register a new CodeFRAME account.
|
|
342
|
+
|
|
343
|
+
Creates a new account and automatically logs you in.
|
|
344
|
+
|
|
345
|
+
Examples:
|
|
346
|
+
|
|
347
|
+
codeframe auth register
|
|
348
|
+
|
|
349
|
+
codeframe auth register --email new@example.com --password secret
|
|
350
|
+
"""
|
|
351
|
+
# Prompt for email if not provided
|
|
352
|
+
if not email:
|
|
353
|
+
email = typer.prompt("Email")
|
|
354
|
+
|
|
355
|
+
# Prompt for password if not provided
|
|
356
|
+
if not password:
|
|
357
|
+
password = typer.prompt("Password", hide_input=True)
|
|
358
|
+
# Confirm password
|
|
359
|
+
password_confirm = typer.prompt("Confirm password", hide_input=True)
|
|
360
|
+
if password != password_confirm:
|
|
361
|
+
console.print("[red]Error:[/red] Passwords don't match")
|
|
362
|
+
raise typer.Exit(1)
|
|
363
|
+
|
|
364
|
+
# Call register API
|
|
365
|
+
base_url = get_api_base_url()
|
|
366
|
+
register_url = f"{base_url}/auth/register"
|
|
367
|
+
|
|
368
|
+
try:
|
|
369
|
+
response = requests.post(
|
|
370
|
+
register_url,
|
|
371
|
+
json={
|
|
372
|
+
"email": email,
|
|
373
|
+
"password": password,
|
|
374
|
+
},
|
|
375
|
+
headers={"Content-Type": "application/json"},
|
|
376
|
+
timeout=30,
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
if response.status_code == 201:
|
|
380
|
+
console.print(f"[green]✓ Account registered successfully for {email}[/green]")
|
|
381
|
+
|
|
382
|
+
# Auto-login after registration
|
|
383
|
+
console.print("Logging in...")
|
|
384
|
+
login_url = f"{base_url}/auth/jwt/login"
|
|
385
|
+
login_response = requests.post(
|
|
386
|
+
login_url,
|
|
387
|
+
data={"username": email, "password": password},
|
|
388
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
389
|
+
timeout=30,
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
if login_response.status_code == 200:
|
|
393
|
+
token = login_response.json().get("access_token")
|
|
394
|
+
if token:
|
|
395
|
+
store_token(token)
|
|
396
|
+
console.print("[green]✓ Logged in automatically[/green]")
|
|
397
|
+
else:
|
|
398
|
+
console.print("[yellow]Note:[/yellow] Please log in manually: codeframe auth login")
|
|
399
|
+
|
|
400
|
+
elif response.status_code == 400:
|
|
401
|
+
error_detail = response.json().get("detail", "")
|
|
402
|
+
if "ALREADY_EXISTS" in str(error_detail).upper():
|
|
403
|
+
console.print("[red]Error:[/red] An account with this email already exists")
|
|
404
|
+
else:
|
|
405
|
+
console.print(f"[red]Error:[/red] Registration failed: {error_detail}")
|
|
406
|
+
raise typer.Exit(1)
|
|
407
|
+
|
|
408
|
+
elif response.status_code == 422:
|
|
409
|
+
# Validation error
|
|
410
|
+
error_data = response.json()
|
|
411
|
+
console.print("[red]Error:[/red] Invalid registration data")
|
|
412
|
+
if "detail" in error_data:
|
|
413
|
+
for error in error_data["detail"]:
|
|
414
|
+
field = error.get("loc", ["", ""])[-1]
|
|
415
|
+
msg = error.get("msg", "")
|
|
416
|
+
console.print(f" - {field}: {msg}")
|
|
417
|
+
raise typer.Exit(1)
|
|
418
|
+
|
|
419
|
+
else:
|
|
420
|
+
console.print(f"[red]Error:[/red] Registration failed (HTTP {response.status_code})")
|
|
421
|
+
raise typer.Exit(1)
|
|
422
|
+
|
|
423
|
+
except requests.ConnectionError:
|
|
424
|
+
console.print(f"[red]Error:[/red] Cannot connect to server at {base_url}")
|
|
425
|
+
console.print("Make sure the CodeFRAME server is running: codeframe serve")
|
|
426
|
+
raise typer.Exit(1)
|
|
427
|
+
|
|
428
|
+
except requests.Timeout:
|
|
429
|
+
console.print("[red]Error:[/red] Request timed out")
|
|
430
|
+
raise typer.Exit(1)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
@auth_app.command()
|
|
434
|
+
def whoami():
|
|
435
|
+
"""Show current logged-in user.
|
|
436
|
+
|
|
437
|
+
Displays your email and account information.
|
|
438
|
+
|
|
439
|
+
Example:
|
|
440
|
+
|
|
441
|
+
codeframe auth whoami
|
|
442
|
+
"""
|
|
443
|
+
# Check if authenticated
|
|
444
|
+
if not is_authenticated():
|
|
445
|
+
console.print("[yellow]Not logged in.[/yellow]")
|
|
446
|
+
console.print("Please log in: codeframe auth login")
|
|
447
|
+
raise typer.Exit(1)
|
|
448
|
+
|
|
449
|
+
# Get user info
|
|
450
|
+
try:
|
|
451
|
+
client = APIClient()
|
|
452
|
+
user = client.get("/users/me")
|
|
453
|
+
|
|
454
|
+
console.print(f"\n[bold]Logged in as:[/bold] {user.get('email', 'Unknown')}")
|
|
455
|
+
if user.get("id"):
|
|
456
|
+
console.print(f"[bold]User ID:[/bold] {user['id']}")
|
|
457
|
+
console.print()
|
|
458
|
+
|
|
459
|
+
except AuthenticationError:
|
|
460
|
+
console.print("[yellow]Session expired.[/yellow]")
|
|
461
|
+
console.print("Please log in again: codeframe auth login")
|
|
462
|
+
raise typer.Exit(1)
|
|
463
|
+
|
|
464
|
+
except Exception as e:
|
|
465
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
466
|
+
raise typer.Exit(1)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
# =============================================================================
|
|
470
|
+
# Credential Management Commands
|
|
471
|
+
# =============================================================================
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
@auth_app.command("setup")
|
|
475
|
+
def setup_credential(
|
|
476
|
+
provider: Optional[str] = typer.Option(
|
|
477
|
+
None, "--provider", "-p", help="Provider name (e.g., anthropic, github, openai)"
|
|
478
|
+
),
|
|
479
|
+
value: Optional[str] = typer.Option(
|
|
480
|
+
None, "--value", "-v", help="Credential value (API key or token)", hide_input=True
|
|
481
|
+
),
|
|
482
|
+
):
|
|
483
|
+
"""Configure a credential for a provider.
|
|
484
|
+
|
|
485
|
+
Securely stores API keys and tokens for LLM providers, Git services,
|
|
486
|
+
and other integrations. Credentials are stored in the system keyring
|
|
487
|
+
or an encrypted file.
|
|
488
|
+
|
|
489
|
+
Examples:
|
|
490
|
+
|
|
491
|
+
codeframe auth setup # Interactive mode
|
|
492
|
+
|
|
493
|
+
codeframe auth setup --provider anthropic --value sk-ant-...
|
|
494
|
+
|
|
495
|
+
codeframe auth setup -p github -v ghp_...
|
|
496
|
+
"""
|
|
497
|
+
manager = CredentialManager()
|
|
498
|
+
|
|
499
|
+
# Interactive provider selection if not provided
|
|
500
|
+
if not provider:
|
|
501
|
+
console.print("\n[bold]Select a provider to configure:[/bold]\n")
|
|
502
|
+
providers = [
|
|
503
|
+
("1", "anthropic", "Anthropic (Claude) - LLM API"),
|
|
504
|
+
("2", "openai", "OpenAI (GPT) - LLM API"),
|
|
505
|
+
("3", "github", "GitHub - Git integration"),
|
|
506
|
+
("4", "gitlab", "GitLab - Git integration"),
|
|
507
|
+
]
|
|
508
|
+
for num, _, desc in providers:
|
|
509
|
+
console.print(f" {num}. {desc}")
|
|
510
|
+
|
|
511
|
+
choice = typer.prompt("\nEnter number or provider name", default="1")
|
|
512
|
+
|
|
513
|
+
# Map number to provider
|
|
514
|
+
choice_map = {p[0]: p[1] for p in providers}
|
|
515
|
+
provider = choice_map.get(choice, choice)
|
|
516
|
+
|
|
517
|
+
# Resolve provider name
|
|
518
|
+
try:
|
|
519
|
+
provider_enum = resolve_provider_name(provider)
|
|
520
|
+
except ValueError as e:
|
|
521
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
522
|
+
raise typer.Exit(1)
|
|
523
|
+
|
|
524
|
+
# Prompt for value if not provided
|
|
525
|
+
if not value:
|
|
526
|
+
console.print(f"\nConfiguring [bold]{provider_enum.display_name}[/bold]")
|
|
527
|
+
|
|
528
|
+
# Show environment variable hint
|
|
529
|
+
env_hint = f"(or set {provider_enum.env_var} environment variable)"
|
|
530
|
+
console.print(f"[dim]{env_hint}[/dim]\n")
|
|
531
|
+
|
|
532
|
+
value = typer.prompt(
|
|
533
|
+
"Enter credential value",
|
|
534
|
+
hide_input=True,
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
# Reject empty or whitespace-only values
|
|
538
|
+
if not value or not value.strip():
|
|
539
|
+
console.print("[red]Error:[/red] Credential value cannot be empty")
|
|
540
|
+
raise typer.Exit(1)
|
|
541
|
+
|
|
542
|
+
# Validate format
|
|
543
|
+
if not manager.validate_credential_format(provider_enum, value):
|
|
544
|
+
console.print("[red]Error:[/red] Invalid credential format")
|
|
545
|
+
console.print("Please check the value and try again.")
|
|
546
|
+
raise typer.Exit(1)
|
|
547
|
+
|
|
548
|
+
# Store credential
|
|
549
|
+
manager.set_credential(provider_enum, value)
|
|
550
|
+
console.print(f"[green]Successfully stored credential for {provider_enum.display_name}[/green]")
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
@auth_app.command("list")
|
|
554
|
+
def list_credentials():
|
|
555
|
+
"""List all configured credentials.
|
|
556
|
+
|
|
557
|
+
Shows credentials from both environment variables and secure storage.
|
|
558
|
+
Values are masked for security.
|
|
559
|
+
|
|
560
|
+
Example:
|
|
561
|
+
|
|
562
|
+
codeframe auth list
|
|
563
|
+
"""
|
|
564
|
+
manager = CredentialManager()
|
|
565
|
+
credentials = manager.list_credentials()
|
|
566
|
+
|
|
567
|
+
if not credentials:
|
|
568
|
+
console.print("\n[yellow]No credentials configured.[/yellow]")
|
|
569
|
+
console.print("Use 'codeframe auth setup' to configure credentials.\n")
|
|
570
|
+
return
|
|
571
|
+
|
|
572
|
+
# Create table
|
|
573
|
+
table = Table(title="Configured Credentials")
|
|
574
|
+
table.add_column("Provider", style="cyan")
|
|
575
|
+
table.add_column("Source", style="green")
|
|
576
|
+
table.add_column("Value (masked)", style="dim")
|
|
577
|
+
table.add_column("Status")
|
|
578
|
+
|
|
579
|
+
for cred in credentials:
|
|
580
|
+
source_str = "env" if cred.source == CredentialSource.ENVIRONMENT else "stored"
|
|
581
|
+
|
|
582
|
+
if cred.is_expired:
|
|
583
|
+
status = "[red]expired[/red]"
|
|
584
|
+
else:
|
|
585
|
+
status = "[green]valid[/green]"
|
|
586
|
+
|
|
587
|
+
table.add_row(
|
|
588
|
+
cred.provider.display_name,
|
|
589
|
+
source_str,
|
|
590
|
+
cred.masked_value or "***",
|
|
591
|
+
status,
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
console.print()
|
|
595
|
+
console.print(table)
|
|
596
|
+
console.print()
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
@auth_app.command("validate")
|
|
600
|
+
def validate_credential(
|
|
601
|
+
provider: str = typer.Argument(..., help="Provider to validate (e.g., anthropic, github)"),
|
|
602
|
+
):
|
|
603
|
+
"""Validate a credential by testing it with the provider.
|
|
604
|
+
|
|
605
|
+
Makes a minimal API call to verify the credential is working.
|
|
606
|
+
|
|
607
|
+
Examples:
|
|
608
|
+
|
|
609
|
+
codeframe auth validate anthropic
|
|
610
|
+
|
|
611
|
+
codeframe auth validate github
|
|
612
|
+
"""
|
|
613
|
+
manager = CredentialManager()
|
|
614
|
+
|
|
615
|
+
# Resolve provider
|
|
616
|
+
try:
|
|
617
|
+
provider_enum = resolve_provider_name(provider)
|
|
618
|
+
except ValueError as e:
|
|
619
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
620
|
+
raise typer.Exit(1)
|
|
621
|
+
|
|
622
|
+
# Get credential
|
|
623
|
+
value = manager.get_credential(provider_enum)
|
|
624
|
+
if not value:
|
|
625
|
+
console.print(f"[red]Error:[/red] No credential configured for {provider_enum.display_name}")
|
|
626
|
+
console.print("Use 'codeframe auth setup' to configure it, or set the environment variable.")
|
|
627
|
+
raise typer.Exit(1)
|
|
628
|
+
|
|
629
|
+
console.print(f"Validating {provider_enum.display_name} credential...")
|
|
630
|
+
|
|
631
|
+
# Validate based on provider type
|
|
632
|
+
if provider_enum == CredentialProvider.LLM_ANTHROPIC:
|
|
633
|
+
valid, message = validate_anthropic_credential(value)
|
|
634
|
+
elif provider_enum == CredentialProvider.LLM_OPENAI:
|
|
635
|
+
valid, message = validate_openai_credential(value)
|
|
636
|
+
elif provider_enum == CredentialProvider.GIT_GITHUB:
|
|
637
|
+
valid, message = validate_github_credential(value)
|
|
638
|
+
else:
|
|
639
|
+
# Generic format validation for unsupported providers
|
|
640
|
+
valid = manager.validate_credential_format(provider_enum, value)
|
|
641
|
+
message = "Format appears valid" if valid else "Invalid format"
|
|
642
|
+
|
|
643
|
+
if valid:
|
|
644
|
+
console.print(f"[green]{message}[/green]")
|
|
645
|
+
else:
|
|
646
|
+
console.print(f"[red]Error:[/red] {message}")
|
|
647
|
+
raise typer.Exit(1)
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
@auth_app.command("rotate")
|
|
651
|
+
def rotate_credential(
|
|
652
|
+
provider: str = typer.Argument(..., help="Provider to rotate credential for"),
|
|
653
|
+
value: Optional[str] = typer.Option(
|
|
654
|
+
None, "--value", "-v", help="New credential value", hide_input=True
|
|
655
|
+
),
|
|
656
|
+
force: bool = typer.Option(
|
|
657
|
+
False, "--force", "-f", help="Skip API validation of new credential"
|
|
658
|
+
),
|
|
659
|
+
):
|
|
660
|
+
"""Rotate a credential with a new value.
|
|
661
|
+
|
|
662
|
+
Validates the new credential before storing (unless --force is used).
|
|
663
|
+
The old credential is only replaced after successful validation.
|
|
664
|
+
|
|
665
|
+
Examples:
|
|
666
|
+
|
|
667
|
+
codeframe auth rotate anthropic --value sk-ant-new-...
|
|
668
|
+
|
|
669
|
+
codeframe auth rotate github -v ghp_new_token --force
|
|
670
|
+
"""
|
|
671
|
+
manager = CredentialManager()
|
|
672
|
+
|
|
673
|
+
# Resolve provider
|
|
674
|
+
try:
|
|
675
|
+
provider_enum = resolve_provider_name(provider)
|
|
676
|
+
except ValueError as e:
|
|
677
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
678
|
+
raise typer.Exit(1)
|
|
679
|
+
|
|
680
|
+
# Check if credential exists
|
|
681
|
+
source = manager.get_credential_source(provider_enum)
|
|
682
|
+
if source == CredentialSource.NOT_FOUND:
|
|
683
|
+
console.print(f"[yellow]Note:[/yellow] No existing credential for {provider_enum.display_name}")
|
|
684
|
+
console.print("Use 'codeframe auth setup' to create a new credential.")
|
|
685
|
+
raise typer.Exit(1)
|
|
686
|
+
|
|
687
|
+
# Prompt for new value if not provided
|
|
688
|
+
if not value:
|
|
689
|
+
value = typer.prompt("Enter new credential value", hide_input=True)
|
|
690
|
+
value = value.strip()
|
|
691
|
+
# Validate format
|
|
692
|
+
if not manager.validate_credential_format(provider_enum, value):
|
|
693
|
+
console.print("[red]Error:[/red] Invalid credential format")
|
|
694
|
+
raise typer.Exit(1)
|
|
695
|
+
|
|
696
|
+
# Validate with API unless --force
|
|
697
|
+
if not force:
|
|
698
|
+
console.print("Validating new credential...")
|
|
699
|
+
|
|
700
|
+
if provider_enum == CredentialProvider.LLM_ANTHROPIC:
|
|
701
|
+
valid, message = validate_anthropic_credential(value)
|
|
702
|
+
elif provider_enum == CredentialProvider.LLM_OPENAI:
|
|
703
|
+
valid, message = validate_openai_credential(value)
|
|
704
|
+
elif provider_enum == CredentialProvider.GIT_GITHUB:
|
|
705
|
+
valid, message = validate_github_credential(value)
|
|
706
|
+
else:
|
|
707
|
+
valid = True
|
|
708
|
+
message = "Skipping API validation for this provider type"
|
|
709
|
+
|
|
710
|
+
if not valid:
|
|
711
|
+
console.print(f"[red]Error:[/red] {message}")
|
|
712
|
+
console.print("Use --force to skip validation.")
|
|
713
|
+
raise typer.Exit(1)
|
|
714
|
+
|
|
715
|
+
# Rotate credential
|
|
716
|
+
manager.rotate_credential(provider_enum, value)
|
|
717
|
+
console.print(f"[green]Successfully rotated credential for {provider_enum.display_name}[/green]")
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
@auth_app.command("remove")
|
|
721
|
+
def remove_credential(
|
|
722
|
+
provider: str = typer.Argument(..., help="Provider to remove credential for"),
|
|
723
|
+
yes: bool = typer.Option(
|
|
724
|
+
False, "--yes", "-y", help="Skip confirmation prompt"
|
|
725
|
+
),
|
|
726
|
+
):
|
|
727
|
+
"""Remove a stored credential.
|
|
728
|
+
|
|
729
|
+
Note: This only removes credentials from secure storage.
|
|
730
|
+
Environment variables are not affected.
|
|
731
|
+
|
|
732
|
+
Examples:
|
|
733
|
+
|
|
734
|
+
codeframe auth remove anthropic
|
|
735
|
+
|
|
736
|
+
codeframe auth remove github --yes
|
|
737
|
+
"""
|
|
738
|
+
manager = CredentialManager()
|
|
739
|
+
|
|
740
|
+
# Resolve provider
|
|
741
|
+
try:
|
|
742
|
+
provider_enum = resolve_provider_name(provider)
|
|
743
|
+
except ValueError as e:
|
|
744
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
745
|
+
raise typer.Exit(1)
|
|
746
|
+
|
|
747
|
+
# Check if credential exists in storage (not just environment)
|
|
748
|
+
source = manager.get_credential_source(provider_enum)
|
|
749
|
+
if source != CredentialSource.STORED:
|
|
750
|
+
if source == CredentialSource.ENVIRONMENT:
|
|
751
|
+
console.print(
|
|
752
|
+
f"[yellow]Credential for {provider_enum.display_name} is set via environment "
|
|
753
|
+
f"variable ({provider_enum.env_var}) and cannot be removed by this command[/yellow]"
|
|
754
|
+
)
|
|
755
|
+
else:
|
|
756
|
+
console.print(f"[yellow]No stored credential found for {provider_enum.display_name}[/yellow]")
|
|
757
|
+
return
|
|
758
|
+
|
|
759
|
+
# Confirm deletion
|
|
760
|
+
if not yes:
|
|
761
|
+
confirm = typer.confirm(
|
|
762
|
+
f"Remove stored credential for {provider_enum.display_name}?"
|
|
763
|
+
)
|
|
764
|
+
if not confirm:
|
|
765
|
+
console.print("Cancelled.")
|
|
766
|
+
return
|
|
767
|
+
|
|
768
|
+
# Delete credential
|
|
769
|
+
try:
|
|
770
|
+
manager.delete_credential(provider_enum)
|
|
771
|
+
console.print(f"[green]Removed stored credential for {provider_enum.display_name}[/green]")
|
|
772
|
+
except Exception as e:
|
|
773
|
+
logger.debug(f"Credential deletion error: {e}")
|
|
774
|
+
console.print("[red]Error:[/red] Failed to remove stored credential")
|
|
775
|
+
raise typer.Exit(1)
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
# =============================================================================
|
|
779
|
+
# API Key Management Commands
|
|
780
|
+
# =============================================================================
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
@auth_app.command("api-key-create")
|
|
784
|
+
def api_key_create(
|
|
785
|
+
name: str = typer.Option(..., "--name", "-n", help="Human-readable name for the key"),
|
|
786
|
+
user_id: int = typer.Option(..., "--user-id", "-u", help="User ID to create key for"),
|
|
787
|
+
scopes: Optional[str] = typer.Option(
|
|
788
|
+
None, "--scopes", "-s", help="Comma-separated scopes (default: read,write)"
|
|
789
|
+
),
|
|
790
|
+
):
|
|
791
|
+
"""Create a new API key.
|
|
792
|
+
|
|
793
|
+
Creates a new API key for programmatic access to the CodeFRAME API.
|
|
794
|
+
The full key is displayed only once - store it securely.
|
|
795
|
+
|
|
796
|
+
Examples:
|
|
797
|
+
|
|
798
|
+
codeframe auth api-key-create --name "CI Key" --user-id 1
|
|
799
|
+
|
|
800
|
+
codeframe auth api-key-create -n "Read Only" -u 1 -s read
|
|
801
|
+
"""
|
|
802
|
+
from codeframe.auth.api_keys import SCOPE_READ, SCOPE_WRITE, validate_scopes
|
|
803
|
+
|
|
804
|
+
# Parse scopes
|
|
805
|
+
if scopes:
|
|
806
|
+
scope_list = [s.strip() for s in scopes.split(",")]
|
|
807
|
+
if not validate_scopes(scope_list):
|
|
808
|
+
console.print("[red]Error:[/red] Invalid scopes. Valid scopes: read, write, admin")
|
|
809
|
+
raise typer.Exit(1)
|
|
810
|
+
else:
|
|
811
|
+
scope_list = [SCOPE_READ, SCOPE_WRITE]
|
|
812
|
+
|
|
813
|
+
# Get database and service
|
|
814
|
+
db = get_db_for_cli()
|
|
815
|
+
service = ApiKeyService(db)
|
|
816
|
+
|
|
817
|
+
try:
|
|
818
|
+
result = service.create_api_key(
|
|
819
|
+
user_id=user_id,
|
|
820
|
+
name=name,
|
|
821
|
+
scopes=scope_list,
|
|
822
|
+
)
|
|
823
|
+
|
|
824
|
+
console.print("\n[green]API key created successfully![/green]\n")
|
|
825
|
+
console.print(f"[bold]Key:[/bold] {result.key}")
|
|
826
|
+
console.print(f"[bold]ID:[/bold] {result.id}")
|
|
827
|
+
console.print(f"[bold]Prefix:[/bold] {result.prefix}")
|
|
828
|
+
console.print()
|
|
829
|
+
console.print("[yellow]Save this key securely - it will not be shown again.[/yellow]\n")
|
|
830
|
+
|
|
831
|
+
except ValueError as e:
|
|
832
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
833
|
+
raise typer.Exit(1)
|
|
834
|
+
except Exception as e:
|
|
835
|
+
logger.debug(f"API key creation error: {e}")
|
|
836
|
+
console.print("[red]Error:[/red] Failed to create API key")
|
|
837
|
+
raise typer.Exit(1)
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
@auth_app.command("api-key-list")
|
|
841
|
+
def api_key_list(
|
|
842
|
+
user_id: int = typer.Option(..., "--user-id", "-u", help="User ID to list keys for"),
|
|
843
|
+
):
|
|
844
|
+
"""List all API keys for a user.
|
|
845
|
+
|
|
846
|
+
Shows API key metadata without exposing the full key or hash.
|
|
847
|
+
|
|
848
|
+
Example:
|
|
849
|
+
|
|
850
|
+
codeframe auth api-key-list --user-id 1
|
|
851
|
+
"""
|
|
852
|
+
# Get database and service
|
|
853
|
+
db = get_db_for_cli()
|
|
854
|
+
service = ApiKeyService(db)
|
|
855
|
+
|
|
856
|
+
keys = service.list_api_keys(user_id=user_id)
|
|
857
|
+
|
|
858
|
+
if not keys:
|
|
859
|
+
console.print("\n[yellow]No API keys found.[/yellow]\n")
|
|
860
|
+
return
|
|
861
|
+
|
|
862
|
+
# Create table
|
|
863
|
+
table = Table(title="API Keys")
|
|
864
|
+
table.add_column("ID", style="cyan")
|
|
865
|
+
table.add_column("Name", style="green")
|
|
866
|
+
table.add_column("Prefix", style="dim")
|
|
867
|
+
table.add_column("Scopes")
|
|
868
|
+
table.add_column("Created")
|
|
869
|
+
table.add_column("Last Used")
|
|
870
|
+
table.add_column("Status")
|
|
871
|
+
|
|
872
|
+
for key in keys:
|
|
873
|
+
status = "[green]active[/green]" if key.is_active else "[red]revoked[/red]"
|
|
874
|
+
last_used = key.last_used_at or "never"
|
|
875
|
+
scopes_str = ", ".join(key.scopes)
|
|
876
|
+
|
|
877
|
+
table.add_row(
|
|
878
|
+
key.id,
|
|
879
|
+
key.name,
|
|
880
|
+
key.prefix,
|
|
881
|
+
scopes_str,
|
|
882
|
+
key.created_at[:10] if key.created_at else "",
|
|
883
|
+
last_used[:10] if last_used != "never" else last_used,
|
|
884
|
+
status,
|
|
885
|
+
)
|
|
886
|
+
|
|
887
|
+
console.print()
|
|
888
|
+
console.print(table)
|
|
889
|
+
console.print()
|
|
890
|
+
|
|
891
|
+
|
|
892
|
+
@auth_app.command("api-key-revoke")
|
|
893
|
+
def api_key_revoke(
|
|
894
|
+
key_id: str = typer.Argument(..., help="API key ID to revoke"),
|
|
895
|
+
user_id: int = typer.Option(..., "--user-id", "-u", help="User ID (must own the key)"),
|
|
896
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
|
|
897
|
+
):
|
|
898
|
+
"""Revoke an API key.
|
|
899
|
+
|
|
900
|
+
Revoked keys can no longer be used for authentication.
|
|
901
|
+
|
|
902
|
+
Examples:
|
|
903
|
+
|
|
904
|
+
codeframe auth api-key-revoke abc123 --user-id 1 --yes
|
|
905
|
+
|
|
906
|
+
codeframe auth api-key-revoke abc123 -u 1
|
|
907
|
+
"""
|
|
908
|
+
# Confirm revocation
|
|
909
|
+
if not yes:
|
|
910
|
+
confirm = typer.confirm(f"Revoke API key {key_id}?")
|
|
911
|
+
if not confirm:
|
|
912
|
+
console.print("Cancelled.")
|
|
913
|
+
return
|
|
914
|
+
|
|
915
|
+
# Get database and service
|
|
916
|
+
db = get_db_for_cli()
|
|
917
|
+
service = ApiKeyService(db)
|
|
918
|
+
|
|
919
|
+
success = service.revoke_api_key(key_id, user_id=user_id)
|
|
920
|
+
|
|
921
|
+
if success:
|
|
922
|
+
console.print(f"[green]API key {key_id} revoked successfully.[/green]")
|
|
923
|
+
else:
|
|
924
|
+
console.print("[red]Error:[/red] API key not found or not owned by user")
|
|
925
|
+
raise typer.Exit(1)
|
|
926
|
+
|
|
927
|
+
|
|
928
|
+
@auth_app.command("api-key-rotate")
|
|
929
|
+
def api_key_rotate(
|
|
930
|
+
key_id: str = typer.Argument(..., help="API key ID to rotate"),
|
|
931
|
+
user_id: int = typer.Option(..., "--user-id", "-u", help="User ID (must own the key)"),
|
|
932
|
+
):
|
|
933
|
+
"""Rotate an API key.
|
|
934
|
+
|
|
935
|
+
Creates a new key with the same name and scopes, then revokes the old key.
|
|
936
|
+
Use this when a key may have been compromised.
|
|
937
|
+
|
|
938
|
+
Example:
|
|
939
|
+
|
|
940
|
+
codeframe auth api-key-rotate abc123 --user-id 1
|
|
941
|
+
"""
|
|
942
|
+
# Get database and service
|
|
943
|
+
db = get_db_for_cli()
|
|
944
|
+
service = ApiKeyService(db)
|
|
945
|
+
|
|
946
|
+
result = service.rotate_api_key(key_id, user_id=user_id)
|
|
947
|
+
|
|
948
|
+
if result:
|
|
949
|
+
console.print("\n[green]API key rotated successfully![/green]\n")
|
|
950
|
+
console.print(f"[bold]New Key:[/bold] {result.key}")
|
|
951
|
+
console.print(f"[bold]New ID:[/bold] {result.id}")
|
|
952
|
+
console.print(f"[bold]Prefix:[/bold] {result.prefix}")
|
|
953
|
+
console.print()
|
|
954
|
+
console.print("[yellow]Save this key securely - it will not be shown again.[/yellow]")
|
|
955
|
+
console.print("[dim]The old key has been revoked.[/dim]\n")
|
|
956
|
+
else:
|
|
957
|
+
console.print("[red]Error:[/red] API key not found or not owned by user")
|
|
958
|
+
raise typer.Exit(1)
|