golf-mcp 0.1.11__py3-none-any.whl → 0.1.13__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.
Potentially problematic release.
This version of golf-mcp might be problematic. Click here for more details.
- golf/__init__.py +1 -1
- golf/auth/__init__.py +38 -26
- golf/auth/api_key.py +16 -23
- golf/auth/helpers.py +68 -54
- golf/auth/oauth.py +340 -277
- golf/auth/provider.py +58 -53
- golf/cli/__init__.py +1 -1
- golf/cli/main.py +209 -87
- golf/commands/__init__.py +1 -1
- golf/commands/build.py +31 -25
- golf/commands/init.py +81 -53
- golf/commands/run.py +30 -15
- golf/core/__init__.py +1 -1
- golf/core/builder.py +493 -362
- golf/core/builder_auth.py +115 -107
- golf/core/builder_telemetry.py +12 -9
- golf/core/config.py +62 -46
- golf/core/parser.py +174 -136
- golf/core/telemetry.py +216 -95
- golf/core/transformer.py +53 -55
- golf/examples/__init__.py +0 -1
- golf/examples/api_key/pre_build.py +2 -2
- golf/examples/api_key/tools/issues/create.py +35 -36
- golf/examples/api_key/tools/issues/list.py +42 -37
- golf/examples/api_key/tools/repos/list.py +50 -29
- golf/examples/api_key/tools/search/code.py +50 -37
- golf/examples/api_key/tools/users/get.py +21 -20
- golf/examples/basic/pre_build.py +4 -4
- golf/examples/basic/prompts/welcome.py +6 -7
- golf/examples/basic/resources/current_time.py +10 -9
- golf/examples/basic/resources/info.py +6 -5
- golf/examples/basic/resources/weather/common.py +16 -10
- golf/examples/basic/resources/weather/current.py +15 -11
- golf/examples/basic/resources/weather/forecast.py +15 -11
- golf/examples/basic/tools/github_user.py +19 -21
- golf/examples/basic/tools/hello.py +10 -6
- golf/examples/basic/tools/payments/charge.py +34 -25
- golf/examples/basic/tools/payments/common.py +8 -6
- golf/examples/basic/tools/payments/refund.py +29 -25
- golf/telemetry/__init__.py +6 -6
- golf/telemetry/instrumentation.py +455 -310
- {golf_mcp-0.1.11.dist-info → golf_mcp-0.1.13.dist-info}/METADATA +1 -1
- golf_mcp-0.1.13.dist-info/RECORD +55 -0
- golf_mcp-0.1.11.dist-info/RECORD +0 -55
- {golf_mcp-0.1.11.dist-info → golf_mcp-0.1.13.dist-info}/WHEEL +0 -0
- {golf_mcp-0.1.11.dist-info → golf_mcp-0.1.13.dist-info}/entry_points.txt +0 -0
- {golf_mcp-0.1.11.dist-info → golf_mcp-0.1.13.dist-info}/licenses/LICENSE +0 -0
- {golf_mcp-0.1.11.dist-info → golf_mcp-0.1.13.dist-info}/top_level.txt +0 -0
golf/auth/provider.py
CHANGED
|
@@ -4,107 +4,112 @@ This module defines the ProviderConfig class used to configure
|
|
|
4
4
|
OAuth authentication for GolfMCP servers.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from typing import
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
8
9
|
from pydantic import BaseModel, Field, field_validator
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
class ProviderConfig(BaseModel):
|
|
12
13
|
"""Configuration for an OAuth2 provider.
|
|
13
|
-
|
|
14
|
+
|
|
14
15
|
This class defines the configuration for an OAuth2 provider,
|
|
15
16
|
including the endpoints, credentials, and other settings needed
|
|
16
17
|
to authenticate with the provider.
|
|
17
18
|
"""
|
|
18
|
-
|
|
19
|
+
|
|
19
20
|
# Provider identification
|
|
20
21
|
provider: str = Field(
|
|
21
|
-
...,
|
|
22
|
-
description="Provider type (e.g., 'github', 'google', 'custom')"
|
|
22
|
+
..., description="Provider type (e.g., 'github', 'google', 'custom')"
|
|
23
23
|
)
|
|
24
|
-
|
|
24
|
+
|
|
25
25
|
# OAuth credentials - names of environment variables to read at runtime
|
|
26
|
-
client_id_env_var: str = Field(
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
client_id_env_var: str = Field(
|
|
27
|
+
..., description="Name of environment variable for Client ID"
|
|
28
|
+
)
|
|
29
|
+
client_secret_env_var: str = Field(
|
|
30
|
+
..., description="Name of environment variable for Client Secret"
|
|
31
|
+
)
|
|
32
|
+
|
|
29
33
|
# These fields will store the actual values read at runtime in dist/server.py
|
|
30
34
|
# They are made optional here as they are resolved in the generated code.
|
|
31
|
-
client_id:
|
|
32
|
-
|
|
35
|
+
client_id: str | None = Field(
|
|
36
|
+
None, description="OAuth client ID (resolved at runtime)"
|
|
37
|
+
)
|
|
38
|
+
client_secret: str | None = Field(
|
|
39
|
+
None, description="OAuth client secret (resolved at runtime)"
|
|
40
|
+
)
|
|
33
41
|
|
|
34
42
|
# OAuth endpoints (can be baked in)
|
|
35
43
|
authorize_url: str = Field(..., description="Authorization endpoint URL")
|
|
36
44
|
token_url: str = Field(..., description="Token endpoint URL")
|
|
37
|
-
userinfo_url:
|
|
38
|
-
None,
|
|
39
|
-
description="User info endpoint URL (for OIDC providers)"
|
|
45
|
+
userinfo_url: str | None = Field(
|
|
46
|
+
None, description="User info endpoint URL (for OIDC providers)"
|
|
40
47
|
)
|
|
41
|
-
|
|
42
|
-
jwks_uri:
|
|
43
|
-
None,
|
|
44
|
-
description="JSON Web Key Set URI (for token validation)"
|
|
48
|
+
|
|
49
|
+
jwks_uri: str | None = Field(
|
|
50
|
+
None, description="JSON Web Key Set URI (for token validation)"
|
|
45
51
|
)
|
|
46
|
-
|
|
47
|
-
scopes:
|
|
48
|
-
default_factory=list,
|
|
49
|
-
description="OAuth scopes to request from the provider"
|
|
52
|
+
|
|
53
|
+
scopes: list[str] = Field(
|
|
54
|
+
default_factory=list, description="OAuth scopes to request from the provider"
|
|
50
55
|
)
|
|
51
|
-
|
|
52
|
-
issuer_url:
|
|
53
|
-
None,
|
|
54
|
-
description="OIDC issuer URL for discovery (if using OIDC) - will be overridden by runtime value in server.py"
|
|
56
|
+
|
|
57
|
+
issuer_url: str | None = Field(
|
|
58
|
+
None,
|
|
59
|
+
description="OIDC issuer URL for discovery (if using OIDC) - will be overridden by runtime value in server.py",
|
|
55
60
|
)
|
|
56
|
-
|
|
61
|
+
|
|
57
62
|
callback_path: str = Field(
|
|
58
|
-
"/auth/callback",
|
|
59
|
-
description="Path on this server where the IdP should redirect after authentication"
|
|
63
|
+
"/auth/callback",
|
|
64
|
+
description="Path on this server where the IdP should redirect after authentication",
|
|
60
65
|
)
|
|
61
|
-
|
|
66
|
+
|
|
62
67
|
# JWT configuration
|
|
63
|
-
jwt_secret_env_var: str = Field(
|
|
64
|
-
|
|
68
|
+
jwt_secret_env_var: str = Field(
|
|
69
|
+
..., description="Name of environment variable for JWT Secret"
|
|
70
|
+
)
|
|
71
|
+
jwt_secret: str | None = Field(
|
|
72
|
+
None, description="Secret key for signing JWT tokens (resolved at runtime)"
|
|
73
|
+
)
|
|
65
74
|
token_expiration: int = Field(
|
|
66
|
-
3600,
|
|
67
|
-
description="JWT token expiration time in seconds",
|
|
68
|
-
ge=60,
|
|
69
|
-
le=86400
|
|
75
|
+
3600, description="JWT token expiration time in seconds", ge=60, le=86400
|
|
70
76
|
)
|
|
71
|
-
|
|
72
|
-
settings:
|
|
73
|
-
default_factory=dict,
|
|
74
|
-
description="Additional provider-specific settings"
|
|
77
|
+
|
|
78
|
+
settings: dict[str, Any] = Field(
|
|
79
|
+
default_factory=dict, description="Additional provider-specific settings"
|
|
75
80
|
)
|
|
76
|
-
|
|
77
|
-
@field_validator(
|
|
81
|
+
|
|
82
|
+
@field_validator("provider")
|
|
78
83
|
@classmethod
|
|
79
84
|
def validate_provider(cls, value: str) -> str:
|
|
80
85
|
"""Validate the provider type.
|
|
81
|
-
|
|
86
|
+
|
|
82
87
|
Ensures the provider type is a valid, supported provider.
|
|
83
|
-
|
|
88
|
+
|
|
84
89
|
Args:
|
|
85
90
|
value: The provider type
|
|
86
|
-
|
|
91
|
+
|
|
87
92
|
Returns:
|
|
88
93
|
The validated provider type
|
|
89
|
-
|
|
94
|
+
|
|
90
95
|
Raises:
|
|
91
96
|
ValueError: If the provider type is not supported
|
|
92
97
|
"""
|
|
93
|
-
known_providers = {
|
|
94
|
-
|
|
95
|
-
if value not in known_providers and not value.startswith(
|
|
98
|
+
known_providers = {"custom", "github", "google", "jwks"}
|
|
99
|
+
|
|
100
|
+
if value not in known_providers and not value.startswith("custom:"):
|
|
96
101
|
raise ValueError(
|
|
97
102
|
f"Unknown provider: '{value}'. Must be one of {known_providers} "
|
|
98
103
|
"or start with 'custom:'"
|
|
99
104
|
)
|
|
100
105
|
return value
|
|
101
|
-
|
|
106
|
+
|
|
102
107
|
def get_provider_name(self) -> str:
|
|
103
108
|
"""Get a clean provider name for display purposes.
|
|
104
|
-
|
|
109
|
+
|
|
105
110
|
Returns:
|
|
106
111
|
A human-readable provider name
|
|
107
112
|
"""
|
|
108
|
-
if self.provider.startswith(
|
|
113
|
+
if self.provider.startswith("custom:"):
|
|
109
114
|
return self.provider[7:] # Remove 'custom:' prefix
|
|
110
|
-
return self.provider.capitalize()
|
|
115
|
+
return self.provider.capitalize()
|
golf/cli/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"""CLI package for the GolfMCP framework."""
|
|
1
|
+
"""CLI package for the GolfMCP framework."""
|
golf/cli/main.py
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
"""CLI entry points for GolfMCP."""
|
|
2
2
|
|
|
3
|
+
import atexit
|
|
3
4
|
import os
|
|
4
5
|
from pathlib import Path
|
|
5
|
-
from typing import Optional
|
|
6
|
-
import atexit
|
|
7
6
|
|
|
8
7
|
import typer
|
|
9
8
|
from rich.console import Console
|
|
10
9
|
from rich.panel import Panel
|
|
11
10
|
|
|
12
11
|
from golf import __version__
|
|
13
|
-
from golf.core.config import
|
|
14
|
-
from golf.core.telemetry import
|
|
12
|
+
from golf.core.config import find_project_root, load_settings
|
|
13
|
+
from golf.core.telemetry import (
|
|
14
|
+
is_telemetry_enabled,
|
|
15
|
+
set_telemetry_enabled,
|
|
16
|
+
shutdown,
|
|
17
|
+
track_event,
|
|
18
|
+
track_detailed_error,
|
|
19
|
+
_detect_execution_environment,
|
|
20
|
+
)
|
|
15
21
|
|
|
16
22
|
# Create console for rich output
|
|
17
23
|
console = Console()
|
|
@@ -48,24 +54,39 @@ def callback(
|
|
|
48
54
|
False, "--verbose", "-v", help="Increase verbosity of output."
|
|
49
55
|
),
|
|
50
56
|
no_telemetry: bool = typer.Option(
|
|
51
|
-
False,
|
|
57
|
+
False,
|
|
58
|
+
"--no-telemetry",
|
|
59
|
+
help="Disable telemetry collection (persists for future commands).",
|
|
60
|
+
),
|
|
61
|
+
test: bool = typer.Option(
|
|
62
|
+
False,
|
|
63
|
+
"--test",
|
|
64
|
+
hidden=True,
|
|
65
|
+
help="Run in test mode (disables telemetry for this execution only).",
|
|
52
66
|
),
|
|
53
67
|
) -> None:
|
|
54
68
|
"""GolfMCP: A Pythonic framework for building MCP servers with zero boilerplate."""
|
|
55
69
|
# Set verbosity in environment for other components to access
|
|
56
70
|
if verbose:
|
|
57
71
|
os.environ["GOLF_VERBOSE"] = "1"
|
|
58
|
-
|
|
59
|
-
# Set
|
|
72
|
+
|
|
73
|
+
# Set test mode if flag is used (temporary, just for this execution)
|
|
74
|
+
if test:
|
|
75
|
+
set_telemetry_enabled(False, persist=False)
|
|
76
|
+
os.environ["GOLF_TEST_MODE"] = "1"
|
|
77
|
+
|
|
78
|
+
# Set telemetry preference if flag is used (permanent)
|
|
60
79
|
if no_telemetry:
|
|
61
80
|
set_telemetry_enabled(False, persist=True)
|
|
62
|
-
console.print(
|
|
81
|
+
console.print(
|
|
82
|
+
"[dim]Telemetry has been disabled. You can re-enable it with: golf telemetry enable[/dim]"
|
|
83
|
+
)
|
|
63
84
|
|
|
64
85
|
|
|
65
86
|
@app.command()
|
|
66
87
|
def init(
|
|
67
88
|
project_name: str = typer.Argument(..., help="Name of the project to create"),
|
|
68
|
-
output_dir:
|
|
89
|
+
output_dir: Path | None = typer.Option(
|
|
69
90
|
None, "--output-dir", "-o", help="Directory to create the project in"
|
|
70
91
|
),
|
|
71
92
|
template: str = typer.Option(
|
|
@@ -73,19 +94,21 @@ def init(
|
|
|
73
94
|
),
|
|
74
95
|
) -> None:
|
|
75
96
|
"""Initialize a new GolfMCP project.
|
|
76
|
-
|
|
97
|
+
|
|
77
98
|
Creates a new directory with the project scaffold, including
|
|
78
99
|
examples for tools, resources, and prompts.
|
|
79
100
|
"""
|
|
80
101
|
# Import here to avoid circular imports
|
|
81
102
|
from golf.commands.init import initialize_project
|
|
82
|
-
|
|
103
|
+
|
|
83
104
|
# Use the current directory if no output directory is specified
|
|
84
105
|
if output_dir is None:
|
|
85
106
|
output_dir = Path.cwd() / project_name
|
|
86
|
-
|
|
107
|
+
|
|
87
108
|
# Execute the initialization command (it handles its own tracking)
|
|
88
|
-
initialize_project(
|
|
109
|
+
initialize_project(
|
|
110
|
+
project_name=project_name, output_dir=output_dir, template=template
|
|
111
|
+
)
|
|
89
112
|
|
|
90
113
|
|
|
91
114
|
# Create a build group with subcommands
|
|
@@ -95,179 +118,278 @@ app.add_typer(build_app, name="build")
|
|
|
95
118
|
|
|
96
119
|
@build_app.command("dev")
|
|
97
120
|
def build_dev(
|
|
98
|
-
output_dir:
|
|
121
|
+
output_dir: str | None = typer.Option(
|
|
99
122
|
None, "--output-dir", "-o", help="Directory to output the built project"
|
|
100
|
-
)
|
|
101
|
-
):
|
|
123
|
+
),
|
|
124
|
+
) -> None:
|
|
102
125
|
"""Build a development version with environment variables copied."""
|
|
103
126
|
# Find project root directory
|
|
104
127
|
project_root, config_path = find_project_root()
|
|
105
|
-
|
|
128
|
+
|
|
106
129
|
if not project_root:
|
|
107
|
-
console.print(
|
|
130
|
+
console.print(
|
|
131
|
+
"[bold red]Error: No GolfMCP project found in the current directory or any parent directory.[/bold red]"
|
|
132
|
+
)
|
|
108
133
|
console.print("Run 'golf init <project_name>' to create a new project.")
|
|
109
|
-
track_event(
|
|
134
|
+
track_event(
|
|
135
|
+
"cli_build_failed",
|
|
136
|
+
{
|
|
137
|
+
"success": False,
|
|
138
|
+
"environment": "dev",
|
|
139
|
+
"error_type": "NoProjectFound",
|
|
140
|
+
"error_message": "No GolfMCP project found",
|
|
141
|
+
},
|
|
142
|
+
)
|
|
110
143
|
raise typer.Exit(code=1)
|
|
111
|
-
|
|
144
|
+
|
|
112
145
|
# Load settings from the found project
|
|
113
146
|
settings = load_settings(project_root)
|
|
114
|
-
|
|
147
|
+
|
|
115
148
|
# Set default output directory if not specified
|
|
116
|
-
if output_dir is None
|
|
117
|
-
|
|
118
|
-
else:
|
|
119
|
-
output_dir = Path(output_dir)
|
|
120
|
-
|
|
149
|
+
output_dir = project_root / "dist" if output_dir is None else Path(output_dir)
|
|
150
|
+
|
|
121
151
|
try:
|
|
122
152
|
# Build the project with environment variables copied
|
|
123
153
|
from golf.commands.build import build_project
|
|
124
|
-
|
|
154
|
+
|
|
155
|
+
build_project(
|
|
156
|
+
project_root, settings, output_dir, build_env="dev", copy_env=True
|
|
157
|
+
)
|
|
125
158
|
# Track successful build with environment
|
|
126
159
|
track_event("cli_build_success", {"success": True, "environment": "dev"})
|
|
127
160
|
except Exception as e:
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
161
|
+
track_detailed_error(
|
|
162
|
+
"cli_build_failed",
|
|
163
|
+
e,
|
|
164
|
+
context="Development build with environment variables",
|
|
165
|
+
operation="build_dev",
|
|
166
|
+
additional_props={"environment": "dev", "copy_env": True}
|
|
167
|
+
)
|
|
131
168
|
raise
|
|
132
169
|
|
|
133
170
|
|
|
134
171
|
@build_app.command("prod")
|
|
135
172
|
def build_prod(
|
|
136
|
-
output_dir:
|
|
173
|
+
output_dir: str | None = typer.Option(
|
|
137
174
|
None, "--output-dir", "-o", help="Directory to output the built project"
|
|
138
|
-
)
|
|
139
|
-
):
|
|
175
|
+
),
|
|
176
|
+
) -> None:
|
|
140
177
|
"""Build a production version without copying environment variables."""
|
|
141
178
|
# Find project root directory
|
|
142
179
|
project_root, config_path = find_project_root()
|
|
143
|
-
|
|
180
|
+
|
|
144
181
|
if not project_root:
|
|
145
|
-
console.print(
|
|
182
|
+
console.print(
|
|
183
|
+
"[bold red]Error: No GolfMCP project found in the current directory or any parent directory.[/bold red]"
|
|
184
|
+
)
|
|
146
185
|
console.print("Run 'golf init <project_name>' to create a new project.")
|
|
147
|
-
track_event(
|
|
186
|
+
track_event(
|
|
187
|
+
"cli_build_failed",
|
|
188
|
+
{
|
|
189
|
+
"success": False,
|
|
190
|
+
"environment": "prod",
|
|
191
|
+
"error_type": "NoProjectFound",
|
|
192
|
+
"error_message": "No GolfMCP project found",
|
|
193
|
+
},
|
|
194
|
+
)
|
|
148
195
|
raise typer.Exit(code=1)
|
|
149
|
-
|
|
196
|
+
|
|
150
197
|
# Load settings from the found project
|
|
151
198
|
settings = load_settings(project_root)
|
|
152
|
-
|
|
199
|
+
|
|
153
200
|
# Set default output directory if not specified
|
|
154
|
-
if output_dir is None
|
|
155
|
-
|
|
156
|
-
else:
|
|
157
|
-
output_dir = Path(output_dir)
|
|
158
|
-
|
|
201
|
+
output_dir = project_root / "dist" if output_dir is None else Path(output_dir)
|
|
202
|
+
|
|
159
203
|
try:
|
|
160
204
|
# Build the project without copying environment variables
|
|
161
205
|
from golf.commands.build import build_project
|
|
162
|
-
|
|
206
|
+
|
|
207
|
+
build_project(
|
|
208
|
+
project_root, settings, output_dir, build_env="prod", copy_env=False
|
|
209
|
+
)
|
|
163
210
|
# Track successful build with environment
|
|
164
211
|
track_event("cli_build_success", {"success": True, "environment": "prod"})
|
|
165
212
|
except Exception as e:
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
213
|
+
track_detailed_error(
|
|
214
|
+
"cli_build_failed",
|
|
215
|
+
e,
|
|
216
|
+
context="Production build without environment variables",
|
|
217
|
+
operation="build_prod",
|
|
218
|
+
additional_props={"environment": "prod", "copy_env": False}
|
|
219
|
+
)
|
|
169
220
|
raise
|
|
170
221
|
|
|
171
222
|
|
|
172
223
|
@app.command()
|
|
173
224
|
def run(
|
|
174
|
-
dist_dir:
|
|
225
|
+
dist_dir: str | None = typer.Option(
|
|
175
226
|
None, "--dist-dir", "-d", help="Directory containing the built server"
|
|
176
227
|
),
|
|
177
|
-
host:
|
|
228
|
+
host: str | None = typer.Option(
|
|
178
229
|
None, "--host", "-h", help="Host to bind to (overrides settings)"
|
|
179
230
|
),
|
|
180
|
-
port:
|
|
231
|
+
port: int | None = typer.Option(
|
|
181
232
|
None, "--port", "-p", help="Port to bind to (overrides settings)"
|
|
182
233
|
),
|
|
183
234
|
build_first: bool = typer.Option(
|
|
184
235
|
True, "--build/--no-build", help="Build the project before running"
|
|
185
236
|
),
|
|
186
|
-
):
|
|
237
|
+
) -> None:
|
|
187
238
|
"""Run the built FastMCP server.
|
|
188
|
-
|
|
239
|
+
|
|
189
240
|
This command runs the built server from the dist directory.
|
|
190
241
|
By default, it will build the project first if needed.
|
|
191
242
|
"""
|
|
192
243
|
# Find project root directory
|
|
193
244
|
project_root, config_path = find_project_root()
|
|
194
|
-
|
|
245
|
+
|
|
195
246
|
if not project_root:
|
|
196
|
-
console.print(
|
|
247
|
+
console.print(
|
|
248
|
+
"[bold red]Error: No GolfMCP project found in the current directory or any parent directory.[/bold red]"
|
|
249
|
+
)
|
|
197
250
|
console.print("Run 'golf init <project_name>' to create a new project.")
|
|
198
|
-
track_event(
|
|
251
|
+
track_event(
|
|
252
|
+
"cli_run_failed",
|
|
253
|
+
{
|
|
254
|
+
"success": False,
|
|
255
|
+
"error_type": "NoProjectFound",
|
|
256
|
+
"error_message": "No GolfMCP project found",
|
|
257
|
+
},
|
|
258
|
+
)
|
|
199
259
|
raise typer.Exit(code=1)
|
|
200
|
-
|
|
260
|
+
|
|
201
261
|
# Load settings from the found project
|
|
202
262
|
settings = load_settings(project_root)
|
|
203
|
-
|
|
263
|
+
|
|
204
264
|
# Set default dist directory if not specified
|
|
205
|
-
if dist_dir is None
|
|
206
|
-
|
|
207
|
-
else:
|
|
208
|
-
dist_dir = Path(dist_dir)
|
|
209
|
-
|
|
265
|
+
dist_dir = project_root / "dist" if dist_dir is None else Path(dist_dir)
|
|
266
|
+
|
|
210
267
|
# Check if dist directory exists
|
|
211
268
|
if not dist_dir.exists():
|
|
212
269
|
if build_first:
|
|
213
|
-
console.print(
|
|
270
|
+
console.print(
|
|
271
|
+
f"[yellow]Dist directory {dist_dir} not found. Building first...[/yellow]"
|
|
272
|
+
)
|
|
214
273
|
try:
|
|
215
274
|
# Build the project
|
|
216
275
|
from golf.commands.build import build_project
|
|
276
|
+
|
|
217
277
|
build_project(project_root, settings, dist_dir)
|
|
218
278
|
except Exception as e:
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
279
|
+
console.print(
|
|
280
|
+
f"[bold red]Error building project:[/bold red] {str(e)}"
|
|
281
|
+
)
|
|
282
|
+
track_detailed_error(
|
|
283
|
+
"cli_run_failed",
|
|
284
|
+
e,
|
|
285
|
+
context="Auto-build before running server",
|
|
286
|
+
operation="auto_build_before_run",
|
|
287
|
+
additional_props={"auto_build": True}
|
|
288
|
+
)
|
|
223
289
|
raise
|
|
224
290
|
else:
|
|
225
|
-
console.print(
|
|
226
|
-
|
|
227
|
-
|
|
291
|
+
console.print(
|
|
292
|
+
f"[bold red]Error: Dist directory {dist_dir} not found.[/bold red]"
|
|
293
|
+
)
|
|
294
|
+
console.print(
|
|
295
|
+
"Run 'golf build' first or use --build to build automatically."
|
|
296
|
+
)
|
|
297
|
+
track_event(
|
|
298
|
+
"cli_run_failed",
|
|
299
|
+
{
|
|
300
|
+
"success": False,
|
|
301
|
+
"error_type": "DistNotFound",
|
|
302
|
+
"error_message": "Dist directory not found",
|
|
303
|
+
},
|
|
304
|
+
)
|
|
228
305
|
raise typer.Exit(code=1)
|
|
229
|
-
|
|
306
|
+
|
|
230
307
|
try:
|
|
231
308
|
# Import and run the server
|
|
232
309
|
from golf.commands.run import run_server
|
|
310
|
+
|
|
233
311
|
return_code = run_server(
|
|
234
312
|
project_path=project_root,
|
|
235
313
|
settings=settings,
|
|
236
314
|
dist_dir=dist_dir,
|
|
237
315
|
host=host,
|
|
238
|
-
port=port
|
|
316
|
+
port=port,
|
|
239
317
|
)
|
|
240
|
-
|
|
241
|
-
# Track based on return code
|
|
318
|
+
|
|
319
|
+
# Track based on return code with better categorization
|
|
242
320
|
if return_code == 0:
|
|
243
321
|
track_event("cli_run_success", {"success": True})
|
|
322
|
+
elif return_code in [130, 143, 137, 2]:
|
|
323
|
+
# Intentional shutdowns (not errors):
|
|
324
|
+
# 130: Ctrl+C (SIGINT)
|
|
325
|
+
# 143: SIGTERM (graceful shutdown, e.g., Kubernetes, Docker)
|
|
326
|
+
# 137: SIGKILL (forced shutdown)
|
|
327
|
+
# 2: General interrupt/graceful shutdown
|
|
328
|
+
shutdown_type = {
|
|
329
|
+
130: "UserInterrupt",
|
|
330
|
+
143: "GracefulShutdown",
|
|
331
|
+
137: "ForcedShutdown",
|
|
332
|
+
2: "Interrupt"
|
|
333
|
+
}.get(return_code, "GracefulShutdown")
|
|
334
|
+
|
|
335
|
+
track_event(
|
|
336
|
+
"cli_run_shutdown",
|
|
337
|
+
{
|
|
338
|
+
"success": True, # Not an error
|
|
339
|
+
"shutdown_type": shutdown_type,
|
|
340
|
+
"exit_code": return_code,
|
|
341
|
+
},
|
|
342
|
+
)
|
|
244
343
|
else:
|
|
245
|
-
|
|
246
|
-
|
|
344
|
+
# Actual errors (unexpected exit codes)
|
|
345
|
+
track_event(
|
|
346
|
+
"cli_run_failed",
|
|
347
|
+
{
|
|
348
|
+
"success": False,
|
|
349
|
+
"error_type": "UnexpectedExit",
|
|
350
|
+
"error_message": f"Server process exited unexpectedly with code {return_code}",
|
|
351
|
+
"exit_code": return_code,
|
|
352
|
+
"operation": "server_process_execution",
|
|
353
|
+
"context": "Server process terminated with unexpected exit code",
|
|
354
|
+
# Add execution environment context
|
|
355
|
+
"execution_env": _detect_execution_environment(),
|
|
356
|
+
},
|
|
357
|
+
)
|
|
358
|
+
|
|
247
359
|
# Exit with the same code as the server
|
|
248
360
|
if return_code != 0:
|
|
249
361
|
raise typer.Exit(code=return_code)
|
|
250
362
|
except Exception as e:
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
363
|
+
track_detailed_error(
|
|
364
|
+
"cli_run_failed",
|
|
365
|
+
e,
|
|
366
|
+
context="Server execution or startup failure",
|
|
367
|
+
operation="run_server_execution",
|
|
368
|
+
additional_props={"has_dist_dir": dist_dir.exists() if dist_dir else False}
|
|
369
|
+
)
|
|
254
370
|
raise
|
|
255
371
|
|
|
256
372
|
|
|
257
373
|
# Add telemetry command group
|
|
258
374
|
@app.command()
|
|
259
375
|
def telemetry(
|
|
260
|
-
action: str = typer.Argument(..., help="Action to perform: 'enable' or 'disable'")
|
|
261
|
-
):
|
|
376
|
+
action: str = typer.Argument(..., help="Action to perform: 'enable' or 'disable'"),
|
|
377
|
+
) -> None:
|
|
262
378
|
"""Manage telemetry settings."""
|
|
263
379
|
if action.lower() == "enable":
|
|
264
380
|
set_telemetry_enabled(True, persist=True)
|
|
265
|
-
console.print(
|
|
381
|
+
console.print(
|
|
382
|
+
"[green]✓[/green] Telemetry enabled. Thank you for helping improve Golf!"
|
|
383
|
+
)
|
|
266
384
|
elif action.lower() == "disable":
|
|
267
385
|
set_telemetry_enabled(False, persist=True)
|
|
268
|
-
console.print(
|
|
386
|
+
console.print(
|
|
387
|
+
"[yellow]Telemetry disabled.[/yellow] You can re-enable it anytime with: golf telemetry enable"
|
|
388
|
+
)
|
|
269
389
|
else:
|
|
270
|
-
console.print(
|
|
390
|
+
console.print(
|
|
391
|
+
f"[red]Unknown action '{action}'. Use 'enable' or 'disable'.[/red]"
|
|
392
|
+
)
|
|
271
393
|
raise typer.Exit(code=1)
|
|
272
394
|
|
|
273
395
|
|
|
@@ -280,13 +402,13 @@ if __name__ == "__main__":
|
|
|
280
402
|
border_style="green",
|
|
281
403
|
)
|
|
282
404
|
)
|
|
283
|
-
|
|
405
|
+
|
|
284
406
|
# Add telemetry notice if enabled
|
|
285
407
|
if is_telemetry_enabled():
|
|
286
408
|
console.print(
|
|
287
409
|
"[dim]📊 Anonymous usage data is collected to improve Golf. "
|
|
288
410
|
"Disable with: golf telemetry disable[/dim]\n"
|
|
289
411
|
)
|
|
290
|
-
|
|
412
|
+
|
|
291
413
|
# Run the CLI app
|
|
292
|
-
app()
|
|
414
|
+
app()
|
golf/commands/__init__.py
CHANGED