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.
- codespeak_cli-0.2.0.dist-info/METADATA +11 -0
- codespeak_cli-0.2.0.dist-info/RECORD +19 -0
- codespeak_cli-0.2.0.dist-info/WHEEL +4 -0
- codespeak_cli-0.2.0.dist-info/entry_points.txt +3 -0
- console_client/__init__.py +1 -0
- console_client/auth/__init__.py +1 -0
- console_client/auth/auth_manager.py +156 -0
- console_client/auth/callback_server.py +170 -0
- console_client/auth/exceptions.py +83 -0
- console_client/auth/oauth_pkce.py +134 -0
- console_client/auth/token_storage.py +122 -0
- console_client/build_client.py +318 -0
- console_client/build_client_event_converter.py +156 -0
- console_client/client_feature_flags.py +23 -0
- console_client/console_client_logging.py +220 -0
- console_client/console_client_main.py +348 -0
- console_client/os_environment_servicer.py +676 -0
- console_client/sequence_reorder_buffer.py +93 -0
- console_client/version.py +10 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import logging
|
|
3
|
+
import typing
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from datetime import UTC, datetime
|
|
7
|
+
from logging.handlers import RotatingFileHandler
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, override
|
|
10
|
+
|
|
11
|
+
from codespeak_shared import SpanProvider as SharedSpanProvider
|
|
12
|
+
from codespeak_shared.logging import get_span_provider, set_span_provider
|
|
13
|
+
from codespeak_shared.logging.span_provider import SpanProvider
|
|
14
|
+
|
|
15
|
+
from console_client.version import get_current_package_version
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class IndentingFormatter(logging.Formatter):
|
|
19
|
+
def __init__(self, fmt: str | None = None):
|
|
20
|
+
super().__init__(fmt)
|
|
21
|
+
self.indent_level = 0
|
|
22
|
+
self.indent_str = SpanProvider.INDENT_STR
|
|
23
|
+
|
|
24
|
+
def indent_increase(self) -> int:
|
|
25
|
+
self.indent_level += 1
|
|
26
|
+
return self.indent_level
|
|
27
|
+
|
|
28
|
+
def indent_decrease(self) -> int:
|
|
29
|
+
if self.indent_level > 0:
|
|
30
|
+
self.indent_level -= 1
|
|
31
|
+
else:
|
|
32
|
+
raise ValueError("Cannot decrease indent")
|
|
33
|
+
return self.indent_level
|
|
34
|
+
|
|
35
|
+
@override
|
|
36
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
37
|
+
original_msg = super().format(record)
|
|
38
|
+
indent = self.indent_str * self.indent_level
|
|
39
|
+
result_msg = indent + original_msg.replace("\n", "\\n")
|
|
40
|
+
return result_msg
|
|
41
|
+
|
|
42
|
+
@override
|
|
43
|
+
def formatTime(self, record: logging.LogRecord, datefmt: str | None = None) -> str:
|
|
44
|
+
dt = datetime.fromtimestamp(record.created, tz=UTC)
|
|
45
|
+
return dt.isoformat()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class NoOpFormatter(IndentingFormatter):
|
|
49
|
+
@override
|
|
50
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
51
|
+
return record.getMessage()
|
|
52
|
+
|
|
53
|
+
@override
|
|
54
|
+
def indent_increase(self) -> int:
|
|
55
|
+
return 0
|
|
56
|
+
|
|
57
|
+
@override
|
|
58
|
+
def indent_decrease(self) -> int:
|
|
59
|
+
return 0
|
|
60
|
+
|
|
61
|
+
@override
|
|
62
|
+
def formatTime(self, record: logging.LogRecord, datefmt: str | None = None) -> str:
|
|
63
|
+
dt = datetime.fromtimestamp(record.created, tz=UTC)
|
|
64
|
+
return dt.isoformat()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ConsoleClientLoggingUtil:
|
|
68
|
+
_FILE_PATTERN = "%(asctime)s - [%(threadName)s] %(levelname)s - %(name)s - %(message)s"
|
|
69
|
+
_CONSOLE_PATTERN = "%(asctime)s - %(message)s"
|
|
70
|
+
|
|
71
|
+
initialized = False
|
|
72
|
+
tracer = None
|
|
73
|
+
no_file_logging: bool = True
|
|
74
|
+
enable_spans = True
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def initialize_logger(
|
|
78
|
+
cls,
|
|
79
|
+
log_file_path: Path | None,
|
|
80
|
+
enable_console_logging: bool,
|
|
81
|
+
enable_spans: bool = True,
|
|
82
|
+
log_rotation: bool = False,
|
|
83
|
+
):
|
|
84
|
+
"""
|
|
85
|
+
If log_file_path is None, logging to file will be disabled.
|
|
86
|
+
"""
|
|
87
|
+
if log_file_path:
|
|
88
|
+
log_file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
89
|
+
|
|
90
|
+
logger = logging.getLogger() # root logger
|
|
91
|
+
logger.setLevel(logging.INFO)
|
|
92
|
+
logger.handlers.clear()
|
|
93
|
+
|
|
94
|
+
if log_file_path:
|
|
95
|
+
if log_rotation:
|
|
96
|
+
file_handler = RotatingFileHandler(log_file_path, maxBytes=10 * 1024 * 1024, backupCount=100)
|
|
97
|
+
else:
|
|
98
|
+
file_handler = logging.FileHandler(log_file_path)
|
|
99
|
+
|
|
100
|
+
cls.enable_spans = enable_spans
|
|
101
|
+
|
|
102
|
+
if enable_spans:
|
|
103
|
+
formatter = IndentingFormatter(ConsoleClientLoggingUtil._FILE_PATTERN)
|
|
104
|
+
else:
|
|
105
|
+
formatter = logging.Formatter(ConsoleClientLoggingUtil._FILE_PATTERN)
|
|
106
|
+
file_handler.setFormatter(formatter)
|
|
107
|
+
logger.addHandler(file_handler)
|
|
108
|
+
cls.no_file_logging = False
|
|
109
|
+
else:
|
|
110
|
+
cls.no_file_logging = True
|
|
111
|
+
|
|
112
|
+
if enable_console_logging:
|
|
113
|
+
console_handler = logging.StreamHandler()
|
|
114
|
+
formatter = logging.Formatter(ConsoleClientLoggingUtil._CONSOLE_PATTERN)
|
|
115
|
+
console_handler.setFormatter(formatter)
|
|
116
|
+
logger.addHandler(console_handler)
|
|
117
|
+
|
|
118
|
+
cls.initialized = True
|
|
119
|
+
|
|
120
|
+
current_package_name = __name__.split(".")[0]
|
|
121
|
+
logging.getLogger(__class__.__qualname__).info(
|
|
122
|
+
f"Current package ({current_package_name}) version: {get_current_package_version()}"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
class IndentingLogSpanProvider(SharedSpanProvider):
|
|
126
|
+
@contextmanager
|
|
127
|
+
def create_span(self, name: str) -> typing.Iterator[None]:
|
|
128
|
+
ConsoleClientLoggingUtil.enter_span(name)
|
|
129
|
+
try:
|
|
130
|
+
yield None
|
|
131
|
+
finally:
|
|
132
|
+
ConsoleClientLoggingUtil.exit_span(name)
|
|
133
|
+
|
|
134
|
+
set_span_provider(IndentingLogSpanProvider())
|
|
135
|
+
|
|
136
|
+
@classmethod
|
|
137
|
+
def ensure_initialized(cls):
|
|
138
|
+
if not cls.initialized:
|
|
139
|
+
raise ValueError("Please call ConsoleClientLoggingUtil.initialize()")
|
|
140
|
+
|
|
141
|
+
@staticmethod
|
|
142
|
+
def span(name: str) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
143
|
+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
144
|
+
@functools.wraps(func)
|
|
145
|
+
def wrapper(*args: Any, **kwargs: Any):
|
|
146
|
+
def delegate() -> Any:
|
|
147
|
+
return func(*args, **kwargs)
|
|
148
|
+
|
|
149
|
+
ConsoleClientLoggingUtil.run_in_span(delegate, name, func)
|
|
150
|
+
|
|
151
|
+
return wrapper
|
|
152
|
+
|
|
153
|
+
return decorator
|
|
154
|
+
|
|
155
|
+
@classmethod
|
|
156
|
+
def enter_span(cls, name: str, annotated_function: Callable[..., Any] | None = None):
|
|
157
|
+
cls.ensure_initialized()
|
|
158
|
+
cause = f"annotated function: {annotated_function.__qualname__}" if annotated_function else "explicitly"
|
|
159
|
+
|
|
160
|
+
# for unnamed spans, just increase indent
|
|
161
|
+
if name:
|
|
162
|
+
logging.getLogger(ConsoleClientLoggingUtil.__class__.__qualname__).info(f"{name}")
|
|
163
|
+
new_depth = ConsoleClientLoggingUtil._find_indenter().indent_increase()
|
|
164
|
+
|
|
165
|
+
logging.getLogger(ConsoleClientLoggingUtil.__class__.__qualname__).debug(
|
|
166
|
+
f"Started span: {name if name else 'empty'}, {cause}, new depth: {new_depth}"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
@classmethod
|
|
170
|
+
def exit_span(cls, name: str, annotated_function: Callable[..., Any] | None = None):
|
|
171
|
+
ConsoleClientLoggingUtil.ensure_initialized()
|
|
172
|
+
new_depth = ConsoleClientLoggingUtil._find_indenter().indent_decrease()
|
|
173
|
+
cause = f"annotated function: {annotated_function.__qualname__}" if annotated_function else "explicitly"
|
|
174
|
+
logging.getLogger(ConsoleClientLoggingUtil.__class__.__qualname__).debug(
|
|
175
|
+
f"Exited span: {name}, {cause}, new depth: {new_depth}"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
@classmethod
|
|
179
|
+
def _find_indenter(cls) -> IndentingFormatter:
|
|
180
|
+
if not cls.enable_spans or cls.no_file_logging:
|
|
181
|
+
return NoOpFormatter()
|
|
182
|
+
|
|
183
|
+
file_handler = next(
|
|
184
|
+
(handler for handler in logging.getLogger().handlers if isinstance(handler, logging.FileHandler)), None
|
|
185
|
+
)
|
|
186
|
+
if not file_handler:
|
|
187
|
+
raise ValueError("No file handler found")
|
|
188
|
+
assert isinstance(file_handler.formatter, IndentingFormatter)
|
|
189
|
+
return file_handler.formatter or NoOpFormatter()
|
|
190
|
+
|
|
191
|
+
@staticmethod
|
|
192
|
+
def run_in_span(
|
|
193
|
+
func: Callable[..., Any], span_name: str, annotated_function: Callable[..., Any] | None = None
|
|
194
|
+
) -> Any:
|
|
195
|
+
ConsoleClientLoggingUtil.enter_span(span_name, annotated_function)
|
|
196
|
+
try:
|
|
197
|
+
return func()
|
|
198
|
+
finally:
|
|
199
|
+
ConsoleClientLoggingUtil.exit_span(span_name, annotated_function)
|
|
200
|
+
|
|
201
|
+
class Span:
|
|
202
|
+
def __init__(self, name: str) -> None:
|
|
203
|
+
self.exited = False
|
|
204
|
+
self.name = name
|
|
205
|
+
self._span = get_span_provider().create_span(name)
|
|
206
|
+
|
|
207
|
+
def __enter__(self) -> "ConsoleClientLoggingUtil.Span":
|
|
208
|
+
self._span.__enter__()
|
|
209
|
+
return self
|
|
210
|
+
|
|
211
|
+
def enter(self):
|
|
212
|
+
self.__enter__()
|
|
213
|
+
|
|
214
|
+
def exit(self):
|
|
215
|
+
self.__exit__(None, None, None)
|
|
216
|
+
|
|
217
|
+
def __exit__(self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: Any):
|
|
218
|
+
if not self.exited:
|
|
219
|
+
self._span.__exit__(exc_type, exc_val, exc_tb)
|
|
220
|
+
self.exited = True
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"""Console client CLI for CodeSpeak - remote builds and authentication."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from argparse import SUPPRESS, ArgumentParser
|
|
7
|
+
from importlib.metadata import version
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import NoReturn
|
|
10
|
+
|
|
11
|
+
import dotenv
|
|
12
|
+
from api_stubs.setup_grpc import setup_grpc
|
|
13
|
+
from codespeak_shared import BuildResult, LoggingUtil
|
|
14
|
+
from codespeak_shared.change_request_utils import implement_code_change_request
|
|
15
|
+
from codespeak_shared.codespeak_project import FileBasedCodeSpeakProject
|
|
16
|
+
from codespeak_shared.environment_enhancer import enhance_environment_with_api_key
|
|
17
|
+
from codespeak_shared.exceptions import (
|
|
18
|
+
BuildWithExistingChangeRequest,
|
|
19
|
+
CodespeakInternalError,
|
|
20
|
+
CodespeakUserError,
|
|
21
|
+
ProjectNotFoundUserError,
|
|
22
|
+
)
|
|
23
|
+
from codespeak_shared.project_initializer import initialize_project
|
|
24
|
+
from codespeak_shared.shared_feature_flags import SharedFeatureFlags
|
|
25
|
+
from codespeak_shared.utils.colors import Colors, ThemeType
|
|
26
|
+
from codespeak_shared.utils.lenient_namespace import LenientNamespace
|
|
27
|
+
from codespeak_shared.utils.process_util import register_signal_handlers
|
|
28
|
+
from rich.console import Console
|
|
29
|
+
|
|
30
|
+
from console_client.auth.auth_manager import login
|
|
31
|
+
from console_client.auth.token_storage import is_token_expired, load_user_token
|
|
32
|
+
from console_client.client_feature_flags import ConsoleClientFeatureFlags
|
|
33
|
+
from console_client.console_client_logging import ConsoleClientLoggingUtil
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _ensure_authenticated(console: Console, client_feature_flags: ConsoleClientFeatureFlags) -> BuildResult:
|
|
37
|
+
"""
|
|
38
|
+
Ensure user is authenticated. If no token is found, trigger login flow.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
console: Rich console for user output.
|
|
42
|
+
client_feature_flags: Feature flags instance to check if authentication is enabled.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Result indicating authentication success or failure.
|
|
46
|
+
"""
|
|
47
|
+
# Skip authentication check if feature flag is disabled
|
|
48
|
+
if not client_feature_flags.get_flag_value(ConsoleClientFeatureFlags.CONSOLE_CLIENT_SEND_USER_TOKEN):
|
|
49
|
+
return BuildResult.succeeded("authentication-check")
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
token = load_user_token()
|
|
53
|
+
if token:
|
|
54
|
+
if not is_token_expired():
|
|
55
|
+
# Token exists and is valid
|
|
56
|
+
return BuildResult.succeeded("authentication-check")
|
|
57
|
+
|
|
58
|
+
# Token is expired - trigger login
|
|
59
|
+
console.print("[yellow]Your authentication token has expired.[/yellow]")
|
|
60
|
+
console.print("[yellow]Please log in again to continue...[/yellow]\n")
|
|
61
|
+
return _login_user(console, client_feature_flags)
|
|
62
|
+
|
|
63
|
+
# Token is missing or empty - trigger login
|
|
64
|
+
console.print("[yellow]Authentication required.[/yellow]")
|
|
65
|
+
console.print("[yellow]Please log in to continue...[/yellow]\n")
|
|
66
|
+
return _login_user(console, client_feature_flags)
|
|
67
|
+
|
|
68
|
+
except BaseException as e: # noqa: BLE001
|
|
69
|
+
return BuildResult.failed("authentication-check", e)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _login_user(console: Console, client_feature_flags: ConsoleClientFeatureFlags) -> BuildResult:
|
|
73
|
+
"""Execute login flow."""
|
|
74
|
+
if not client_feature_flags.get_flag_value(ConsoleClientFeatureFlags.CONSOLE_CLIENT_SEND_USER_TOKEN):
|
|
75
|
+
console.print("Login disabled")
|
|
76
|
+
return BuildResult.succeeded("login")
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
login(console, client_feature_flags)
|
|
80
|
+
return BuildResult.succeeded("login")
|
|
81
|
+
|
|
82
|
+
except KeyboardInterrupt as e:
|
|
83
|
+
console.print("\n[yellow]Login cancelled by user.[/yellow]")
|
|
84
|
+
return BuildResult.failed("login", e)
|
|
85
|
+
|
|
86
|
+
except CodespeakUserError as e:
|
|
87
|
+
return BuildResult.failed("login", e)
|
|
88
|
+
|
|
89
|
+
except BaseException as e: # noqa: BLE001 (login catch-all block)
|
|
90
|
+
return BuildResult.failed("login", e)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class ArgumentParserWithHelp(ArgumentParser):
|
|
94
|
+
def error(self, message: str) -> NoReturn:
|
|
95
|
+
sys.stderr.write(f"{message}\n")
|
|
96
|
+
sys.stderr.write("\n")
|
|
97
|
+
self.print_help()
|
|
98
|
+
sys.exit(CodespeakUserError.EXIT_CODE)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def main() -> None:
|
|
102
|
+
register_signal_handlers()
|
|
103
|
+
|
|
104
|
+
dotenv.load_dotenv(dotenv.find_dotenv(usecwd=True))
|
|
105
|
+
dotenv.load_dotenv(".env.local", override=True)
|
|
106
|
+
|
|
107
|
+
logger = logging.getLogger("console-client")
|
|
108
|
+
logger.info(f"Invocation: {' '.join([os.path.basename(sys.argv[0])] + sys.argv[1:])}")
|
|
109
|
+
|
|
110
|
+
def add_common_params(parser: ArgumentParser) -> None:
|
|
111
|
+
parser.add_argument("--enable-flag", action="append", help=SUPPRESS)
|
|
112
|
+
parser.add_argument("--disable-flag", action="append", help=SUPPRESS)
|
|
113
|
+
|
|
114
|
+
def add_build_params(parser: ArgumentParser) -> None:
|
|
115
|
+
add_common_params(parser)
|
|
116
|
+
parser.add_argument("--start", help=SUPPRESS)
|
|
117
|
+
parser.add_argument("--until", help=SUPPRESS)
|
|
118
|
+
parser.add_argument("--dry-run", action="store_true", help=SUPPRESS)
|
|
119
|
+
parser.add_argument(
|
|
120
|
+
"--no-diff-check",
|
|
121
|
+
action="store_true",
|
|
122
|
+
help=SUPPRESS,
|
|
123
|
+
)
|
|
124
|
+
parser.add_argument(
|
|
125
|
+
"--no-interactive",
|
|
126
|
+
action="store_true",
|
|
127
|
+
default=None,
|
|
128
|
+
help="run in non-interactive mode without real-time progress updates",
|
|
129
|
+
)
|
|
130
|
+
parser.add_argument(
|
|
131
|
+
"--disable-parallelism",
|
|
132
|
+
action="store_true",
|
|
133
|
+
help=SUPPRESS,
|
|
134
|
+
)
|
|
135
|
+
parser.add_argument(
|
|
136
|
+
"--spec",
|
|
137
|
+
help="path to the specification file to build; if omitted, all registered specification files will be built",
|
|
138
|
+
default=None,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
resume_control_group = parser.add_mutually_exclusive_group()
|
|
142
|
+
resume_control_group.add_argument(
|
|
143
|
+
"--clean",
|
|
144
|
+
action="store_true",
|
|
145
|
+
help=SUPPRESS,
|
|
146
|
+
)
|
|
147
|
+
resume_control_group.add_argument(
|
|
148
|
+
"--retry",
|
|
149
|
+
action="store_true",
|
|
150
|
+
help=SUPPRESS,
|
|
151
|
+
)
|
|
152
|
+
parser.add_argument(
|
|
153
|
+
"--skip-tests",
|
|
154
|
+
action="store_true",
|
|
155
|
+
help="Skip improving test coverage",
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
def add_init_params(parser: ArgumentParser) -> None:
|
|
159
|
+
add_common_params(parser)
|
|
160
|
+
parser.add_argument("--profile", help="configuration profile to use for initialization")
|
|
161
|
+
parser.add_argument(
|
|
162
|
+
"--project-name", default=None, help="custom name for the project (defaults to directory name)"
|
|
163
|
+
)
|
|
164
|
+
parser.add_argument(
|
|
165
|
+
"--mixed",
|
|
166
|
+
action="store_true",
|
|
167
|
+
help="enable mixed mode, allowing both CodeSpeak spec-driven code and manually written code to coexist",
|
|
168
|
+
default=False,
|
|
169
|
+
)
|
|
170
|
+
parser.add_argument(
|
|
171
|
+
"--ignore-enclosing-project-check",
|
|
172
|
+
default=False,
|
|
173
|
+
action="store_true",
|
|
174
|
+
help=SUPPRESS,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Create main parser
|
|
178
|
+
parser = ArgumentParserWithHelp(description="CodeSpeak build system")
|
|
179
|
+
parser.prog = "codespeak"
|
|
180
|
+
parser.add_argument("--version", action="version", version=f"CodeSpeak CLI {version('codespeak-cli')}")
|
|
181
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
182
|
+
|
|
183
|
+
# Init command (local project initialization)
|
|
184
|
+
init_parser = subparsers.add_parser(
|
|
185
|
+
"init",
|
|
186
|
+
help="initialize a new CodeSpeak project",
|
|
187
|
+
description="Initialize a new CodeSpeak project in the current directory. This creates the necessary configuration files and project structure.",
|
|
188
|
+
)
|
|
189
|
+
add_init_params(init_parser)
|
|
190
|
+
|
|
191
|
+
# Login command
|
|
192
|
+
login_parser = subparsers.add_parser(
|
|
193
|
+
"login",
|
|
194
|
+
help="authenticate with CodeSpeak",
|
|
195
|
+
description="Authenticate with CodeSpeak to enable builds. Opens a browser for authentication.",
|
|
196
|
+
)
|
|
197
|
+
add_common_params(login_parser)
|
|
198
|
+
|
|
199
|
+
# Build command (remote build)
|
|
200
|
+
build_parser = subparsers.add_parser(
|
|
201
|
+
"build",
|
|
202
|
+
help="run build",
|
|
203
|
+
description="Build your project according to the specification file(s). CodeSpeak will generate or update code to match the spec.",
|
|
204
|
+
)
|
|
205
|
+
add_build_params(build_parser)
|
|
206
|
+
|
|
207
|
+
# Change command (remote change request)
|
|
208
|
+
change_parser = subparsers.add_parser(
|
|
209
|
+
"change",
|
|
210
|
+
help="code change request",
|
|
211
|
+
description="Request a code change to an existing project. Use --new to create a change request file, or -m to specify the change directly.",
|
|
212
|
+
)
|
|
213
|
+
add_build_params(change_parser)
|
|
214
|
+
change_parser.add_argument(
|
|
215
|
+
"--new",
|
|
216
|
+
action="store_true",
|
|
217
|
+
default=False,
|
|
218
|
+
help="create an empty change request file for further manual editing; after editing, implement the change request with 'codespeak change'",
|
|
219
|
+
)
|
|
220
|
+
change_parser.add_argument("-m", "--message", help="implement a given change request")
|
|
221
|
+
|
|
222
|
+
# Run command (remote build + run target app)
|
|
223
|
+
run_parser = subparsers.add_parser(
|
|
224
|
+
"run",
|
|
225
|
+
help="build and run the target application",
|
|
226
|
+
description="Build and run target application.",
|
|
227
|
+
)
|
|
228
|
+
add_build_params(run_parser)
|
|
229
|
+
|
|
230
|
+
# Parse arguments
|
|
231
|
+
args = parser.parse_args(namespace=LenientNamespace())
|
|
232
|
+
env_vars = dict(os.environ)
|
|
233
|
+
client_feature_flags = ConsoleClientFeatureFlags(env_vars, args.enable_flag, args.disable_flag)
|
|
234
|
+
|
|
235
|
+
# Setup console with theme
|
|
236
|
+
theme = Colors.get_theme(
|
|
237
|
+
ThemeType.DARK
|
|
238
|
+
if client_feature_flags.get_flag_value(SharedFeatureFlags.CONSOLE_USES_DARK_THEME)
|
|
239
|
+
else ThemeType.LIGHT
|
|
240
|
+
)
|
|
241
|
+
console = Console(theme=theme)
|
|
242
|
+
stderr_console = Console(stderr=True, theme=theme)
|
|
243
|
+
|
|
244
|
+
# Print any feature flag warnings
|
|
245
|
+
for warning in client_feature_flags.warnings:
|
|
246
|
+
stderr_console.print(warning)
|
|
247
|
+
|
|
248
|
+
# Handle commands
|
|
249
|
+
try:
|
|
250
|
+
if args.command == "init":
|
|
251
|
+
|
|
252
|
+
def init_logging(local_project: FileBasedCodeSpeakProject, feature_flags: SharedFeatureFlags) -> None:
|
|
253
|
+
ConsoleClientLoggingUtil.initialize_logger(
|
|
254
|
+
local_project.ignored_data_path("codespeak.log").get_underlying_path(), enable_console_logging=False
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
result = initialize_project(
|
|
258
|
+
current_path=Path(os.curdir),
|
|
259
|
+
console=console,
|
|
260
|
+
feature_flags=client_feature_flags,
|
|
261
|
+
args=args,
|
|
262
|
+
initialize_logging=init_logging,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
elif args.command == "login":
|
|
266
|
+
result = _login_user(console, client_feature_flags)
|
|
267
|
+
|
|
268
|
+
elif args.command in ("build", "change", "run"):
|
|
269
|
+
# Find project
|
|
270
|
+
local_project = FileBasedCodeSpeakProject.find_upwards(Path(os.curdir))
|
|
271
|
+
if not local_project:
|
|
272
|
+
result = BuildResult.failed(args.command, ProjectNotFoundUserError(Path(os.curdir).resolve()))
|
|
273
|
+
else:
|
|
274
|
+
ConsoleClientLoggingUtil.initialize_logger(
|
|
275
|
+
local_project.ignored_data_path("codespeak.log").get_underlying_path(), enable_console_logging=False
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
# Enhance environment with API key on client side (before sending to server)
|
|
279
|
+
env_vars = enhance_environment_with_api_key(
|
|
280
|
+
console=console,
|
|
281
|
+
env_vars=env_vars,
|
|
282
|
+
project_root=local_project.project_root.get_underlying_path(),
|
|
283
|
+
no_interactive=args.no_interactive,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
def call_server() -> BuildResult:
|
|
287
|
+
# Ensure user is authenticated before running build
|
|
288
|
+
auth_result = _ensure_authenticated(console, client_feature_flags)
|
|
289
|
+
if auth_result.enum_type != BuildResult.SUCCEEDED:
|
|
290
|
+
return auth_result
|
|
291
|
+
else:
|
|
292
|
+
with LoggingUtil.Span(f"Running command: {args.command}"):
|
|
293
|
+
# setup GRPC logging before importing build_client (which imports grpc)
|
|
294
|
+
setup_grpc(client_feature_flags.get_flag_value(SharedFeatureFlags.DEBUG_GRPC))
|
|
295
|
+
|
|
296
|
+
from console_client.build_client import run_build_client # noqa: PLC0415
|
|
297
|
+
|
|
298
|
+
return run_build_client(args, local_project, client_feature_flags, console, env_vars)
|
|
299
|
+
|
|
300
|
+
if args.command in ("build", "run"):
|
|
301
|
+
ccr_path = local_project.code_change_request_path()
|
|
302
|
+
if local_project.os_env().exists(ccr_path):
|
|
303
|
+
result = BuildResult.failed(
|
|
304
|
+
"build", BuildWithExistingChangeRequest(ccr_path.get_underlying_path())
|
|
305
|
+
)
|
|
306
|
+
else:
|
|
307
|
+
result = call_server()
|
|
308
|
+
else:
|
|
309
|
+
result = implement_code_change_request(local_project, args, console, call_server)
|
|
310
|
+
else:
|
|
311
|
+
# Shouldn't happen as argparse should fail before we get here
|
|
312
|
+
console.print(f"[red]Unknown command: {args.command}[/red]")
|
|
313
|
+
sys.exit(1)
|
|
314
|
+
|
|
315
|
+
except BaseException as e: # noqa: BLE001 (global catch-all block)
|
|
316
|
+
result = BuildResult.failed(args.command, e)
|
|
317
|
+
|
|
318
|
+
# Handle result and exit
|
|
319
|
+
if result.enum_type == BuildResult.SUCCEEDED:
|
|
320
|
+
sys.exit(0)
|
|
321
|
+
elif result.enum_type == BuildResult.SKIPPED:
|
|
322
|
+
sys.exit(0)
|
|
323
|
+
elif result.enum_type == BuildResult.FAILED:
|
|
324
|
+
assert result.exception
|
|
325
|
+
if isinstance(result.exception, KeyboardInterrupt):
|
|
326
|
+
stderr_console.print(
|
|
327
|
+
f"[error.emphasized]{result.command.capitalize()} interrupted by user.[/error.emphasized]"
|
|
328
|
+
)
|
|
329
|
+
sys.exit(130)
|
|
330
|
+
elif isinstance(result.exception, (CodespeakUserError, CodespeakUserError)):
|
|
331
|
+
stderr_console.print(
|
|
332
|
+
f"[error.emphasized]{result.command.capitalize()} failed:[/error.emphasized]\n{result.exception}"
|
|
333
|
+
)
|
|
334
|
+
sys.exit(CodespeakUserError.EXIT_CODE)
|
|
335
|
+
else:
|
|
336
|
+
if client_feature_flags.get_flag_value(ConsoleClientFeatureFlags.PRINT_MESSAGE_FOR_INTERNAL_ERROR):
|
|
337
|
+
stderr_console.print(
|
|
338
|
+
f"[error.emphasized]{result.command.capitalize()} failed:[/error.emphasized]\n{result.exception}"
|
|
339
|
+
)
|
|
340
|
+
else:
|
|
341
|
+
stderr_console.print(
|
|
342
|
+
f"[error.emphasized]{result.command.capitalize()} failed: internal error.[/error.emphasized]"
|
|
343
|
+
)
|
|
344
|
+
sys.exit(CodespeakInternalError.EXIT_CODE)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
if __name__ == "__main__":
|
|
348
|
+
main()
|