codespeak-cli 0.2.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.
@@ -0,0 +1,122 @@
1
+ """Token storage module for managing user authentication tokens."""
2
+
3
+ import json
4
+ import os
5
+ import time
6
+ from pathlib import Path
7
+
8
+ from pydantic import BaseModel
9
+
10
+ # Buffer time in seconds before actual expiry to trigger re-login
11
+ _TOKEN_EXPIRY_BUFFER_SECONDS = 30
12
+
13
+
14
+ class StoredToken(BaseModel):
15
+ """Token data stored in the token file."""
16
+
17
+ access_token: str
18
+ expires_at: int | None = None
19
+
20
+
21
+ def get_codespeak_home() -> Path:
22
+ """Get the CodeSpeak home directory path (~/.codespeak)."""
23
+ return Path.home() / ".codespeak"
24
+
25
+
26
+ def ensure_codespeak_home() -> Path:
27
+ """
28
+ Ensure the CodeSpeak home directory exists.
29
+
30
+ Creates the directory with secure permissions (0700) if it doesn't exist.
31
+
32
+ Returns:
33
+ Path to the CodeSpeak home directory.
34
+ """
35
+ codespeak_home = get_codespeak_home()
36
+ if not codespeak_home.exists():
37
+ codespeak_home.mkdir(parents=True, mode=0o700, exist_ok=True)
38
+ return codespeak_home
39
+
40
+
41
+ def get_token_path() -> Path:
42
+ """Get the path to the token file (~/.codespeak/token.json)."""
43
+ return get_codespeak_home() / "token.json"
44
+
45
+
46
+ def save_token(token: str, expires_in: int = 0) -> None:
47
+ """
48
+ Save the authentication token and its expiration time.
49
+
50
+ Creates the CodeSpeak home directory if it doesn't exist.
51
+ Sets file permissions to 0600 (owner read/write only) for security.
52
+
53
+ Args:
54
+ token: The authentication token to save.
55
+ expires_in: Token lifetime in seconds from now. If 0, no expiry is saved.
56
+
57
+ Raises:
58
+ OSError: If the token cannot be saved due to filesystem errors.
59
+ """
60
+ ensure_codespeak_home()
61
+ token_path = get_token_path()
62
+
63
+ expires_at = int(time.time()) + expires_in if expires_in > 0 else None
64
+ stored = StoredToken(access_token=token, expires_at=expires_at)
65
+
66
+ token_path.write_text(stored.model_dump_json(), encoding="utf-8")
67
+ os.chmod(token_path, 0o600)
68
+
69
+
70
+ def _load_stored_token() -> StoredToken | None:
71
+ """Load the stored token data from the token file."""
72
+ token_path = get_token_path()
73
+ if not token_path.exists():
74
+ return None
75
+
76
+ try:
77
+ data = json.loads(token_path.read_text(encoding="utf-8"))
78
+ return StoredToken.model_validate(data)
79
+ except (json.JSONDecodeError, ValueError):
80
+ return None
81
+
82
+
83
+ def load_user_token() -> str | None:
84
+ """
85
+ Load the authentication token from the token file.
86
+
87
+ Returns:
88
+ The authentication token if it exists, None otherwise.
89
+ """
90
+ stored = _load_stored_token()
91
+ return stored.access_token if stored else None
92
+
93
+
94
+ def delete_token() -> None:
95
+ """
96
+ Delete the authentication token file.
97
+
98
+ Does nothing if the file doesn't exist.
99
+
100
+ Raises:
101
+ OSError: If the file exists but cannot be deleted.
102
+ """
103
+ token_path = get_token_path()
104
+ if token_path.exists():
105
+ token_path.unlink()
106
+
107
+
108
+ def is_token_expired() -> bool:
109
+ """
110
+ Check if the stored token is expired or about to expire.
111
+
112
+ Returns True if the token is expired or will expire within the buffer period,
113
+ giving time for re-login before actual expiry.
114
+
115
+ Returns:
116
+ True if token is expired or will expire soon, False otherwise.
117
+ """
118
+ stored = _load_stored_token()
119
+ if not stored or stored.expires_at is None:
120
+ return False
121
+
122
+ return time.time() >= stored.expires_at - _TOKEN_EXPIRY_BUFFER_SECONDS
@@ -0,0 +1,318 @@
1
+ """
2
+ Build client for connecting to remote CodeSpeak build server.
3
+ """
4
+
5
+ import json
6
+ import logging
7
+ import queue
8
+ import threading
9
+ from argparse import Namespace
10
+ from collections.abc import Iterator
11
+ from typing import Any, cast
12
+
13
+ import grpc
14
+ from api_stubs import os_environment_pb2
15
+ from api_stubs.build_server_pb2 import (
16
+ CancelBuildRequest,
17
+ ClientMessage,
18
+ ServerMessage,
19
+ StartBuildRequest,
20
+ )
21
+ from api_stubs.build_server_pb2_grpc import BuildServiceStub
22
+ from api_stubs.operation_logging import OsEnvRequestLogging
23
+ from api_stubs.os_environment_pb2 import OperationRequest, OperationResponse
24
+ from codespeak_shared import BuildResult
25
+ from codespeak_shared.build_insight.events import (
26
+ BuildInsightEvent,
27
+ ProgressItemCreateEvent,
28
+ ProgressItemUpdateEvent,
29
+ SpanCloseEvent,
30
+ SpanOpenEvent,
31
+ TestsRunEvent,
32
+ TextOutputEvent,
33
+ ToolCallEvent,
34
+ UserVisibleModelOutputEvent,
35
+ )
36
+ from codespeak_shared.build_insight.storage import BuildInsightStorage, BuildInsightStorageMode
37
+ from codespeak_shared.codespeak_project import FileBasedCodeSpeakProject
38
+ from codespeak_shared.env_allowlist import filter_env_vars
39
+ from codespeak_shared.exceptions import (
40
+ BuildStartFailedUserError,
41
+ CodespeakInternalError,
42
+ CodeSpeakServerUnavailableUserError,
43
+ )
44
+ from codespeak_shared.os_environment import OsEnvironment
45
+ from codespeak_shared.progress.rich_progress_output import RichBasedCliProgressRenderer
46
+ from codespeak_shared.protocol_version import CURRENT_VERSION as PROTOCOL_VERSION
47
+ from rich.console import Console
48
+
49
+ from console_client.auth.token_storage import load_user_token
50
+ from console_client.build_client_event_converter import (
51
+ convert_build_finished_proto_to_result,
52
+ convert_proto_to_build_insight_event,
53
+ )
54
+ from console_client.client_feature_flags import ConsoleClientFeatureFlags
55
+ from console_client.os_environment_servicer import OsEnvironmentServicer
56
+ from console_client.sequence_reorder_buffer import SequenceReorderBuffer
57
+
58
+
59
+ def _get_build_insight_event_summary(event: BuildInsightEvent) -> str:
60
+ if isinstance(event, ProgressItemCreateEvent):
61
+ return f"create '{event.title}' [{event.status.value}]"
62
+ elif isinstance(event, ProgressItemUpdateEvent):
63
+ status_text = f" ({event.status_text})" if event.status_text else ""
64
+ return f"update [{event.status.value}]{status_text}"
65
+ elif isinstance(event, ToolCallEvent):
66
+ return f"tool '{event.title}'"
67
+ elif isinstance(event, TextOutputEvent):
68
+ text_preview = event.text[:50] + "..." if len(event.text) > 50 else event.text
69
+ return f"text: {text_preview}"
70
+ elif isinstance(event, UserVisibleModelOutputEvent):
71
+ text_preview = event.text[:50] + "..." if len(event.text) > 50 else event.text
72
+ return f"output: {text_preview}"
73
+ elif isinstance(event, SpanOpenEvent):
74
+ return f"span_open '{event.title}'"
75
+ elif isinstance(event, SpanCloseEvent):
76
+ return f"span_close '{event.span_id[:8]}'"
77
+ elif isinstance(event, TestsRunEvent):
78
+ return "tests_run"
79
+ else:
80
+ return type(event).__name__
81
+
82
+
83
+ def _handle_os_env_request(
84
+ request: OperationRequest,
85
+ os_env: OsEnvironment,
86
+ logger: logging.Logger,
87
+ console: Console,
88
+ ) -> OperationResponse:
89
+ """Handle an OsEnvironment operation request from the server."""
90
+ try:
91
+ # Use OsEnvironmentServicer to handle the request
92
+ servicer = OsEnvironmentServicer(os_env, console)
93
+
94
+ # Handle the request synchronously
95
+ # The servicer's StreamOperations method processes requests one at a time
96
+ def request_iter() -> Iterator[OperationRequest]:
97
+ yield request
98
+
99
+ # Process the request and get the response
100
+ response_iterator = servicer.StreamOperations(request_iter(), None)
101
+ response: OperationResponse = next(response_iterator)
102
+ return response
103
+ except Exception as e: # noqa: BLE001 (os env handler catch-all block)
104
+ # Return a response with client_error set - server will check this field first
105
+ logger.exception(f"Error handling OsEnv request: {e}")
106
+ error_info = os_environment_pb2.ErrorInfo(
107
+ message=f"Client internal error: {type(e).__name__}: {e}",
108
+ type=os_environment_pb2.ErrorType.ERROR_TYPE_UNSPECIFIED,
109
+ )
110
+ return OperationResponse(request_id=request.request_id, client_internal_error=error_info)
111
+
112
+
113
+ def run_build_client(
114
+ args: Namespace,
115
+ local_project: FileBasedCodeSpeakProject,
116
+ client_feature_flags: ConsoleClientFeatureFlags,
117
+ console: Console,
118
+ env: dict[str, str],
119
+ ) -> BuildResult:
120
+ server_host = client_feature_flags.get_flag_value(ConsoleClientFeatureFlags.CONSOLE_CLIENT_BUILD_SERVER_HOST)
121
+ server_port = client_feature_flags.get_flag_value(ConsoleClientFeatureFlags.CONSOLE_CLIENT_BUILD_SERVER_PORT)
122
+ use_secure = client_feature_flags.get_flag_value(ConsoleClientFeatureFlags.CONSOLE_CLIENT_BUILD_SERVER_SECURE)
123
+
124
+ server_address = f"{server_host}:{server_port}"
125
+ console.print(f"Connecting to {server_address}...", highlight=False)
126
+
127
+ # Load authentication token (or send empty token if feature flag is disabled)
128
+ if client_feature_flags.get_flag_value(ConsoleClientFeatureFlags.CONSOLE_CLIENT_SEND_USER_TOKEN):
129
+ user_token = load_user_token() or ""
130
+ else:
131
+ user_token = ""
132
+
133
+ print_grpc_events = client_feature_flags.get_flag_value(ConsoleClientFeatureFlags.CONSOLE_CLIENT_PRINT_GRPC_EVENTS)
134
+
135
+ def print_grpc_event(message: str) -> None:
136
+ console.print(f"[Client] {message}", style="dim")
137
+
138
+ follower_build_insight_storage = BuildInsightStorage(mode=BuildInsightStorageMode.FOLLOWER)
139
+ cli_renderer = RichBasedCliProgressRenderer(
140
+ console, follower_build_insight_storage, interactive=(not args.no_interactive)
141
+ )
142
+
143
+ # Create reorder buffer to handle out-of-order events
144
+ def publish_to_storage(event: BuildInsightEvent) -> None:
145
+ # Events come from the server with sequence numbers already assigned
146
+ follower_build_insight_storage.report_event(event)
147
+
148
+ build_insight_events_buffer = SequenceReorderBuffer[BuildInsightEvent](publish_to_storage)
149
+
150
+ # Connect to server using secure or insecure channel
151
+ if use_secure:
152
+ ssl_credentials = grpc.ssl_channel_credentials()
153
+ channel = grpc.secure_channel(server_address, ssl_credentials)
154
+ else:
155
+ channel = grpc.insecure_channel(server_address)
156
+
157
+ stub = cast(Any, BuildServiceStub(channel))
158
+
159
+ # Create request queue for bidirectional streaming
160
+ # Use None as sentinel to signal end of requests
161
+ request_queue: queue.Queue[ClientMessage | None] = queue.Queue()
162
+
163
+ # Create request iterator
164
+ def request_iterator() -> Iterator[ClientMessage]:
165
+ while True:
166
+ message = request_queue.get()
167
+ if message is None:
168
+ # Sentinel value - stop sending requests
169
+ break
170
+ yield message
171
+
172
+ # Start bidirectional stream
173
+ response_stream = stub.Build(request_iterator())
174
+
175
+ # Serialize args to JSON (convert Namespace to dict)
176
+ args_dict = vars(args)
177
+ args_json = json.dumps(args_dict)
178
+
179
+ # Send StartBuild message with settings, args, feature flags, and user token
180
+ # Convert feature flags to map<string, string> (protobuf requirement)
181
+ feature_flags_str = {k: str(v) for k, v in client_feature_flags.to_dict().items()}
182
+ start_request = ClientMessage(
183
+ start_build=StartBuildRequest(
184
+ project_root=str(local_project.project_root.get_underlying_path().resolve()),
185
+ settings_json=local_project.settings_json_str(),
186
+ args_json=args_json,
187
+ client_feature_flags=feature_flags_str,
188
+ env_vars=filter_env_vars(env),
189
+ user_token=user_token,
190
+ client_protocol_version=PROTOCOL_VERSION,
191
+ )
192
+ )
193
+ if print_grpc_events:
194
+ print_grpc_event("Sending: start_build")
195
+
196
+ request_queue.put(start_request)
197
+
198
+ result: BuildResult | None = None
199
+ stop_event = threading.Event()
200
+ cancellation_requested = threading.Event()
201
+
202
+ logger = logging.getLogger(__name__)
203
+
204
+ # Process server messages in a separate thread to handle os_env_request
205
+ def process_server_messages() -> None:
206
+ nonlocal result
207
+ try:
208
+ for server_message in response_stream:
209
+ if stop_event.is_set():
210
+ break
211
+
212
+ server_message = cast(ServerMessage, server_message)
213
+ which = server_message.WhichOneof("message")
214
+
215
+ if print_grpc_events and which not in ("os_env_request", "build_insight_event"):
216
+ # Don't print generic "Received" for these since we print detailed "Handling" messages
217
+ print_grpc_event(f"Received: {which}")
218
+
219
+ if which == "build_started":
220
+ build_started = server_message.build_started
221
+ result_type = build_started.WhichOneof("result")
222
+ if result_type == "failure":
223
+ error = BuildStartFailedUserError(build_started.failure.message)
224
+ result = BuildResult.failed("build", error)
225
+ break
226
+ message = f"Remote build started (ID: {build_started.success.build_id})"
227
+ logger.info(message)
228
+ console.print(message, highlight=False)
229
+
230
+ elif which == "build_insight_event":
231
+ # Convert protobuf event back to Python and add to reorder buffer
232
+ # The buffer will handle out-of-order delivery and publish to storage in order
233
+ proto_event = server_message.build_insight_event
234
+ py_event = convert_proto_to_build_insight_event(proto_event)
235
+ if print_grpc_events:
236
+ summary = _get_build_insight_event_summary(py_event)
237
+ print_grpc_event(f"Handling build_insight_event: {summary}")
238
+ build_insight_events_buffer.add(py_event)
239
+
240
+ elif which == "os_env_request":
241
+ # Handle file operation request from server
242
+ os_request = cast(os_environment_pb2.OperationRequest, getattr(server_message, "os_env_request"))
243
+ if print_grpc_events:
244
+ request_description = OsEnvRequestLogging.format(os_request)
245
+ print_grpc_event(f"Handling OsEnv request: {request_description}")
246
+ os_response = _handle_os_env_request(os_request, local_project.os_env(), logger, console)
247
+ # Send response back to server (don't log responses)
248
+ client_msg = ClientMessage(os_env_response=os_response)
249
+ request_queue.put(client_msg)
250
+
251
+ elif which == "build_finished":
252
+ build_finished = server_message.build_finished
253
+ logger.info(f"Received build finished message: {build_finished}")
254
+ result = convert_build_finished_proto_to_result(build_finished)
255
+ break
256
+
257
+ except grpc.RpcError as e:
258
+ logger.exception(f"Grpc error: {e}")
259
+
260
+ if not stop_event.is_set():
261
+ # Check if this is a connection error
262
+ if e.code() == grpc.StatusCode.UNAVAILABLE:
263
+ # Convert to user-friendly error
264
+ user_error = CodeSpeakServerUnavailableUserError(server_address)
265
+ result = BuildResult.failed("build", user_error)
266
+ else:
267
+ # Other gRPC errors
268
+ console.print(f"Error during build: {e}", style="bright_red")
269
+ result = BuildResult.failed("build", e)
270
+ except Exception as e: # noqa: BLE001
271
+ if not stop_event.is_set():
272
+ logger.exception(f"Error during build ({type(e).__name__}): {e}")
273
+ console.print(f"Error during build: {e}", style="bright_red")
274
+ result = BuildResult.failed("build", e)
275
+
276
+ # Start processing server messages in a thread
277
+ message_thread = threading.Thread(target=process_server_messages, daemon=False)
278
+ message_thread.start()
279
+
280
+ try:
281
+ while True:
282
+ try:
283
+ # Wait for the thread to complete
284
+ message_thread.join(timeout=0.1)
285
+ if not message_thread.is_alive():
286
+ break
287
+ except KeyboardInterrupt:
288
+ # Do not interrupt client loop immediately.
289
+ # Instead, send build cancellation request to the server and so let it finish the loop
290
+ if not cancellation_requested.is_set():
291
+ cli_renderer.append_text("[yellow]Waiting for server to cancel build...[/yellow]")
292
+ cli_renderer.append_text("[yellow]Press Ctrl+C again to force exit[/yellow]")
293
+ cancellation_requested.set()
294
+ # Send CancelBuild request to server
295
+ cancel_request = ClientMessage(cancel_build=CancelBuildRequest())
296
+ if print_grpc_events:
297
+ print_grpc_event("Sending: cancel_build")
298
+ request_queue.put(cancel_request)
299
+ else:
300
+ # Second Ctrl+C - force exit without waiting for server
301
+ cli_renderer.append_text("[yellow]Force exiting...[/yellow]")
302
+ break
303
+ finally:
304
+ stop_event.set()
305
+ # Send sentinel to stop the request iterator cleanly
306
+ request_queue.put(None)
307
+ cli_renderer.finalize()
308
+ channel.close()
309
+
310
+ if result is None:
311
+ if cancellation_requested.is_set():
312
+ result = BuildResult.failed("build", KeyboardInterrupt())
313
+ else:
314
+ result = BuildResult.failed(
315
+ "build", CodespeakInternalError("Error: connection with server closed before obtaining build result")
316
+ )
317
+
318
+ return result
@@ -0,0 +1,156 @@
1
+ """Converter from protobuf BuildInsightEvent messages to Python objects."""
2
+
3
+ from typing import Any
4
+
5
+ from api_stubs import build_server_pb2
6
+ from api_stubs.build_server_pb2 import BuildFinished, BuildResultType
7
+ from codespeak_shared import BuildResult
8
+ from codespeak_shared.build_insight.events import (
9
+ BuildInsightEvent,
10
+ ProgressItemCreateEvent,
11
+ ProgressItemStatus,
12
+ ProgressItemUpdateEvent,
13
+ SpanCloseEvent,
14
+ SpanOpenEvent,
15
+ StopInteractiveProgressEvent,
16
+ TestsRunEvent,
17
+ TextOutputEvent,
18
+ ToolCallEvent,
19
+ UserVisibleModelOutputEvent,
20
+ )
21
+ from codespeak_shared.exceptions import CodespeakInternalError, CodespeakUserError
22
+ from codespeak_shared.test_runner_types import TestResults
23
+
24
+
25
+ def _convert_progress_status_from_proto(status: int) -> ProgressItemStatus:
26
+ """Convert protobuf enum to Python ProgressItemStatus."""
27
+ mapping: dict[int, ProgressItemStatus] = {
28
+ int(build_server_pb2.PROGRESS_ITEM_STATUS_PENDING): ProgressItemStatus.PENDING,
29
+ int(build_server_pb2.PROGRESS_ITEM_STATUS_IN_PROGRESS): ProgressItemStatus.IN_PROGRESS,
30
+ int(build_server_pb2.PROGRESS_ITEM_STATUS_SKIPPED): ProgressItemStatus.SKIPPED,
31
+ int(build_server_pb2.PROGRESS_ITEM_STATUS_DONE): ProgressItemStatus.DONE,
32
+ int(build_server_pb2.PROGRESS_ITEM_STATUS_FAILED): ProgressItemStatus.FAILED,
33
+ }
34
+ result = mapping.get(status)
35
+ return result if result is not None else ProgressItemStatus.PENDING
36
+
37
+
38
+ def convert_proto_to_build_insight_event(proto_event: Any) -> BuildInsightEvent:
39
+ """Convert a protobuf BuildInsightEvent to a Python BuildInsightEvent."""
40
+ which = proto_event.WhichOneof("event")
41
+
42
+ sequence_number = proto_event.sequence_number
43
+
44
+ if which == "progress_item_create":
45
+ create = proto_event.progress_item_create
46
+ return ProgressItemCreateEvent(
47
+ timestamp=proto_event.timestamp,
48
+ sequence_number=sequence_number,
49
+ progress_item_id=create.progress_item_id,
50
+ status=_convert_progress_status_from_proto(create.status),
51
+ title=create.title,
52
+ description=create.description if create.description else None,
53
+ parent_item_id=create.parent_item_id if create.parent_item_id else None,
54
+ status_text=create.status_text if create.status_text else None,
55
+ )
56
+ elif which == "progress_item_update":
57
+ update = proto_event.progress_item_update
58
+ return ProgressItemUpdateEvent(
59
+ timestamp=proto_event.timestamp,
60
+ sequence_number=sequence_number,
61
+ progress_item_id=update.progress_item_id,
62
+ status=_convert_progress_status_from_proto(update.status),
63
+ status_text=update.status_text if update.status_text else None,
64
+ )
65
+ elif which == "text_output":
66
+ text = proto_event.text_output
67
+ return TextOutputEvent(
68
+ timestamp=proto_event.timestamp,
69
+ sequence_number=sequence_number,
70
+ text=text.text,
71
+ parent_progress_item_id=text.parent_progress_item_id if text.parent_progress_item_id else None,
72
+ )
73
+ elif which == "tool_call":
74
+ tool = proto_event.tool_call
75
+ return ToolCallEvent(
76
+ timestamp=proto_event.timestamp,
77
+ sequence_number=sequence_number,
78
+ title=tool.title,
79
+ details=tool.details if tool.details else None,
80
+ parent_progress_item_id=tool.parent_progress_item_id if tool.parent_progress_item_id else None,
81
+ )
82
+ elif which == "user_visible_model_output":
83
+ model_out = proto_event.user_visible_model_output
84
+ return UserVisibleModelOutputEvent(
85
+ timestamp=proto_event.timestamp,
86
+ sequence_number=sequence_number,
87
+ text=model_out.text,
88
+ parent_progress_item_id=model_out.parent_progress_item_id if model_out.parent_progress_item_id else None,
89
+ )
90
+ elif which == "span_open":
91
+ span = proto_event.span_open
92
+ return SpanOpenEvent(
93
+ timestamp=proto_event.timestamp,
94
+ sequence_number=sequence_number,
95
+ span_id=span.span_id,
96
+ title=span.title,
97
+ description=span.description if span.description else None,
98
+ )
99
+ elif which == "span_close":
100
+ span = proto_event.span_close
101
+ return SpanCloseEvent(
102
+ timestamp=proto_event.timestamp,
103
+ sequence_number=sequence_number,
104
+ span_id=span.span_id,
105
+ )
106
+ elif which == "tests_run":
107
+ tests = proto_event.tests_run
108
+ test_results = None
109
+ if tests.test_results_json:
110
+ test_results = TestResults.model_validate_json(tests.test_results_json)
111
+ return TestsRunEvent(
112
+ timestamp=proto_event.timestamp,
113
+ sequence_number=sequence_number,
114
+ test_results=test_results,
115
+ )
116
+ elif which == "stop_interactive_progress":
117
+ return StopInteractiveProgressEvent(
118
+ timestamp=proto_event.timestamp,
119
+ sequence_number=sequence_number,
120
+ )
121
+ else:
122
+ raise ValueError(f"Unknown type {which} of event {proto_event}")
123
+
124
+
125
+ def convert_build_finished_proto_to_result(build_finished: BuildFinished) -> BuildResult:
126
+ if build_finished.result_type == BuildResultType.BUILD_RESULT_SUCCEEDED:
127
+ result = BuildResult.succeeded(build_finished.command)
128
+ elif build_finished.result_type == BuildResultType.BUILD_RESULT_SKIPPED:
129
+ result = BuildResult(BuildResult.SKIPPED, build_finished.command)
130
+ elif build_finished.result_type == BuildResultType.BUILD_RESULT_CANCELLED:
131
+ result = BuildResult.failed(build_finished.command, KeyboardInterrupt())
132
+ elif build_finished.result_type == BuildResultType.BUILD_RESULT_FAILED:
133
+ if not build_finished.build_error:
134
+ raise ValueError(f"build_finished.build_error is missing in {build_finished}")
135
+
136
+ build_error = build_finished.build_error
137
+
138
+ if build_error.HasField("user_error"):
139
+ exception = CodespeakUserError(user_visible_message=build_error.user_error.user_visible_message)
140
+ elif build_error.HasField("internal_error"):
141
+ exception = CodespeakInternalError(internal_message=build_error.internal_error.internal_message)
142
+ else:
143
+ raise ValueError(f"BuildError has neither user_error nor internal_error: {build_error}")
144
+
145
+ result = BuildResult.failed(build_finished.command, exception)
146
+ else:
147
+ raise ValueError(f"Unexpected build result type: {build_finished.result_type}")
148
+
149
+ # Set build statistics
150
+ if build_finished.build_stats:
151
+ stats = build_finished.build_stats
152
+ result.build_stats.cache_enabled = stats.cache_enabled
153
+ result.build_stats.cache_hits = stats.cache_hits
154
+ result.build_stats.cache_misses = stats.cache_misses
155
+
156
+ return result
@@ -0,0 +1,23 @@
1
+ """Console client feature flags."""
2
+
3
+ from codespeak_shared.shared_feature_flags import FeatureFlag, FeatureFlagsMeta, SharedFeatureFlags
4
+
5
+
6
+ class ConsoleClientFeatureFlags(SharedFeatureFlags, metaclass=FeatureFlagsMeta):
7
+ CONSOLE_CLIENT_PRINT_GRPC_EVENTS = FeatureFlag(default_value=False)
8
+
9
+ CONSOLE_CLIENT_SEND_USER_TOKEN = FeatureFlag(default_value=True)
10
+
11
+ CONSOLE_CLIENT_BUILD_SERVER_HOST = FeatureFlag(default_value="build.codespeak.dev")
12
+
13
+ CONSOLE_CLIENT_BUILD_SERVER_PORT = FeatureFlag(default_value=50053)
14
+
15
+ CONSOLE_CLIENT_BUILD_SERVER_SECURE = FeatureFlag(default_value=True)
16
+
17
+ CONSOLE_CLIENT_AUTH0_DOMAIN = FeatureFlag[str](default_value="codespeak.us.auth0.com")
18
+
19
+ CONSOLE_CLIENT_AUTH0_CLIENT_ID = FeatureFlag[str](default_value="zVHCedn5lS2hl6hiVtQuTMm9Hg2ZHZk9")
20
+
21
+ CONSOLE_CLIENT_AUTH0_SCOPES = FeatureFlag[str](default_value="openid profile email")
22
+
23
+ CONSOLE_CLIENT_AUTH0_CALLBACK_TIMEOUT = FeatureFlag[int](default_value=300)