claude-mpm 4.3.22__py3-none-any.whl → 4.4.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.
- claude_mpm/VERSION +1 -1
- claude_mpm/cli/commands/doctor.py +2 -2
- claude_mpm/hooks/memory_integration_hook.py +1 -1
- claude_mpm/services/agents/memory/content_manager.py +5 -2
- claude_mpm/services/agents/memory/memory_file_service.py +1 -0
- claude_mpm/services/agents/memory/memory_limits_service.py +1 -0
- claude_mpm/services/unified/__init__.py +65 -0
- claude_mpm/services/unified/analyzer_strategies/__init__.py +44 -0
- claude_mpm/services/unified/analyzer_strategies/code_analyzer.py +473 -0
- claude_mpm/services/unified/analyzer_strategies/dependency_analyzer.py +643 -0
- claude_mpm/services/unified/analyzer_strategies/performance_analyzer.py +804 -0
- claude_mpm/services/unified/analyzer_strategies/security_analyzer.py +661 -0
- claude_mpm/services/unified/analyzer_strategies/structure_analyzer.py +696 -0
- claude_mpm/services/unified/deployment_strategies/__init__.py +97 -0
- claude_mpm/services/unified/deployment_strategies/base.py +557 -0
- claude_mpm/services/unified/deployment_strategies/cloud_strategies.py +486 -0
- claude_mpm/services/unified/deployment_strategies/local.py +594 -0
- claude_mpm/services/unified/deployment_strategies/utils.py +672 -0
- claude_mpm/services/unified/deployment_strategies/vercel.py +471 -0
- claude_mpm/services/unified/interfaces.py +499 -0
- claude_mpm/services/unified/migration.py +532 -0
- claude_mpm/services/unified/strategies.py +551 -0
- claude_mpm/services/unified/unified_analyzer.py +534 -0
- claude_mpm/services/unified/unified_config.py +688 -0
- claude_mpm/services/unified/unified_deployment.py +470 -0
- {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.0.dist-info}/METADATA +1 -1
- {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.0.dist-info}/RECORD +31 -12
- {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.0.dist-info}/WHEEL +0 -0
- {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.0.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.0.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,471 @@
|
|
1
|
+
"""
|
2
|
+
Vercel Deployment Strategy
|
3
|
+
==========================
|
4
|
+
|
5
|
+
Handles deployment to Vercel platform for serverless applications.
|
6
|
+
Consolidates Vercel deployment patterns from multiple services.
|
7
|
+
"""
|
8
|
+
|
9
|
+
import json
|
10
|
+
import subprocess
|
11
|
+
import tempfile
|
12
|
+
from datetime import datetime
|
13
|
+
from pathlib import Path
|
14
|
+
from typing import Any, Dict, List, Optional
|
15
|
+
|
16
|
+
from claude_mpm.core.logging_utils import get_logger
|
17
|
+
from claude_mpm.services.unified.strategies import StrategyMetadata, StrategyPriority
|
18
|
+
|
19
|
+
from .base import DeploymentContext, DeploymentResult, DeploymentStrategy, DeploymentType
|
20
|
+
|
21
|
+
|
22
|
+
class VercelDeploymentStrategy(DeploymentStrategy):
|
23
|
+
"""
|
24
|
+
Strategy for Vercel platform deployments.
|
25
|
+
|
26
|
+
Handles deployment of serverless functions, static sites, and
|
27
|
+
full-stack applications to Vercel.
|
28
|
+
|
29
|
+
Features:
|
30
|
+
- Serverless function deployment
|
31
|
+
- Static site deployment
|
32
|
+
- Environment variable management
|
33
|
+
- Custom domain configuration
|
34
|
+
- Deployment previews
|
35
|
+
- Production deployments
|
36
|
+
- Rollback to previous deployments
|
37
|
+
"""
|
38
|
+
|
39
|
+
def __init__(self):
|
40
|
+
"""Initialize Vercel deployment strategy."""
|
41
|
+
metadata = StrategyMetadata(
|
42
|
+
name="VercelDeploymentStrategy",
|
43
|
+
description="Deploy to Vercel serverless platform",
|
44
|
+
supported_types=["application", "service", "agent", "*"],
|
45
|
+
supported_operations=["deploy", "rollback", "verify", "promote"],
|
46
|
+
priority=StrategyPriority.NORMAL,
|
47
|
+
tags={"vercel", "serverless", "cloud", "edge"},
|
48
|
+
)
|
49
|
+
super().__init__(metadata)
|
50
|
+
self._logger = get_logger(f"{__name__}.VercelDeploymentStrategy")
|
51
|
+
self._deployment_urls: Dict[str, str] = {}
|
52
|
+
|
53
|
+
def validate(self, context: DeploymentContext) -> List[str]:
|
54
|
+
"""
|
55
|
+
Validate Vercel deployment configuration.
|
56
|
+
|
57
|
+
Args:
|
58
|
+
context: Deployment context
|
59
|
+
|
60
|
+
Returns:
|
61
|
+
List of validation errors
|
62
|
+
"""
|
63
|
+
errors = []
|
64
|
+
|
65
|
+
# Check Vercel CLI is available
|
66
|
+
if not self._check_vercel_cli():
|
67
|
+
errors.append("Vercel CLI not found. Install with: npm i -g vercel")
|
68
|
+
|
69
|
+
# Check authentication
|
70
|
+
if not self._check_vercel_auth():
|
71
|
+
errors.append("Not authenticated with Vercel. Run: vercel login")
|
72
|
+
|
73
|
+
# Validate source
|
74
|
+
source_path = Path(context.source)
|
75
|
+
if not source_path.exists():
|
76
|
+
errors.append(f"Source does not exist: {source_path}")
|
77
|
+
|
78
|
+
# Check for Vercel configuration
|
79
|
+
vercel_json = source_path / "vercel.json"
|
80
|
+
if not vercel_json.exists():
|
81
|
+
self._logger.warning("No vercel.json found, using defaults")
|
82
|
+
|
83
|
+
# Validate required config
|
84
|
+
config = context.config
|
85
|
+
|
86
|
+
# Check project name
|
87
|
+
if not config.get("project_name"):
|
88
|
+
if not vercel_json.exists():
|
89
|
+
errors.append("Project name required when vercel.json is missing")
|
90
|
+
|
91
|
+
# Validate environment variables
|
92
|
+
env_vars = config.get("env", {})
|
93
|
+
for key, value in env_vars.items():
|
94
|
+
if not isinstance(value, (str, int, bool)):
|
95
|
+
errors.append(f"Invalid env var type for {key}: {type(value)}")
|
96
|
+
|
97
|
+
return errors
|
98
|
+
|
99
|
+
def prepare(self, context: DeploymentContext) -> List[Path]:
|
100
|
+
"""
|
101
|
+
Prepare Vercel deployment artifacts.
|
102
|
+
|
103
|
+
Args:
|
104
|
+
context: Deployment context
|
105
|
+
|
106
|
+
Returns:
|
107
|
+
List of prepared artifact paths
|
108
|
+
"""
|
109
|
+
artifacts = []
|
110
|
+
source_path = Path(context.source)
|
111
|
+
|
112
|
+
# Create deployment directory
|
113
|
+
deploy_dir = Path(tempfile.mkdtemp(prefix="vercel_deploy_"))
|
114
|
+
|
115
|
+
# Copy source to deployment directory
|
116
|
+
if source_path.is_file():
|
117
|
+
# Single file deployment (e.g., serverless function)
|
118
|
+
deploy_file = deploy_dir / source_path.name
|
119
|
+
import shutil
|
120
|
+
shutil.copy2(source_path, deploy_file)
|
121
|
+
artifacts.append(deploy_file)
|
122
|
+
else:
|
123
|
+
# Directory deployment
|
124
|
+
import shutil
|
125
|
+
shutil.copytree(source_path, deploy_dir / "app", dirs_exist_ok=True)
|
126
|
+
artifacts.append(deploy_dir / "app")
|
127
|
+
|
128
|
+
# Create/update vercel.json if needed
|
129
|
+
vercel_config = self._prepare_vercel_config(context, deploy_dir)
|
130
|
+
if vercel_config:
|
131
|
+
artifacts.append(vercel_config)
|
132
|
+
|
133
|
+
# Prepare environment file
|
134
|
+
env_file = self._prepare_env_file(context, deploy_dir)
|
135
|
+
if env_file:
|
136
|
+
artifacts.append(env_file)
|
137
|
+
|
138
|
+
return artifacts
|
139
|
+
|
140
|
+
def execute(
|
141
|
+
self, context: DeploymentContext, artifacts: List[Path]
|
142
|
+
) -> Dict[str, Any]:
|
143
|
+
"""
|
144
|
+
Execute Vercel deployment.
|
145
|
+
|
146
|
+
Args:
|
147
|
+
context: Deployment context
|
148
|
+
artifacts: Prepared artifacts
|
149
|
+
|
150
|
+
Returns:
|
151
|
+
Deployment information
|
152
|
+
"""
|
153
|
+
deployment_id = self._generate_deployment_id()
|
154
|
+
|
155
|
+
# Find deployment directory
|
156
|
+
deploy_dir = artifacts[0].parent if artifacts else Path(tempfile.gettempdir())
|
157
|
+
|
158
|
+
# Build Vercel command
|
159
|
+
cmd = ["vercel"]
|
160
|
+
|
161
|
+
# Add project name if specified
|
162
|
+
if context.config.get("project_name"):
|
163
|
+
cmd.extend(["--name", context.config["project_name"]])
|
164
|
+
|
165
|
+
# Production deployment or preview
|
166
|
+
if context.config.get("production", False):
|
167
|
+
cmd.append("--prod")
|
168
|
+
|
169
|
+
# Skip confirmation
|
170
|
+
cmd.append("--yes")
|
171
|
+
|
172
|
+
# Add token if provided
|
173
|
+
if context.config.get("token"):
|
174
|
+
cmd.extend(["--token", context.config["token"]])
|
175
|
+
|
176
|
+
# Execute deployment
|
177
|
+
self._logger.info(f"Deploying to Vercel: {' '.join(cmd)}")
|
178
|
+
|
179
|
+
try:
|
180
|
+
result = subprocess.run(
|
181
|
+
cmd,
|
182
|
+
cwd=deploy_dir,
|
183
|
+
capture_output=True,
|
184
|
+
text=True,
|
185
|
+
check=True,
|
186
|
+
)
|
187
|
+
|
188
|
+
# Parse deployment URL from output
|
189
|
+
deployment_url = self._parse_deployment_url(result.stdout)
|
190
|
+
|
191
|
+
if deployment_url:
|
192
|
+
self._deployment_urls[deployment_id] = deployment_url
|
193
|
+
self._logger.info(f"Deployment successful: {deployment_url}")
|
194
|
+
|
195
|
+
return {
|
196
|
+
"deployment_id": deployment_id,
|
197
|
+
"deployment_url": deployment_url,
|
198
|
+
"deployed_path": deploy_dir,
|
199
|
+
"production": context.config.get("production", False),
|
200
|
+
"stdout": result.stdout,
|
201
|
+
"timestamp": datetime.now().isoformat(),
|
202
|
+
}
|
203
|
+
else:
|
204
|
+
raise Exception("Could not parse deployment URL from Vercel output")
|
205
|
+
|
206
|
+
except subprocess.CalledProcessError as e:
|
207
|
+
self._logger.error(f"Vercel deployment failed: {e.stderr}")
|
208
|
+
raise Exception(f"Deployment failed: {e.stderr}")
|
209
|
+
|
210
|
+
def verify(
|
211
|
+
self, context: DeploymentContext, deployment_info: Dict[str, Any]
|
212
|
+
) -> bool:
|
213
|
+
"""
|
214
|
+
Verify Vercel deployment success.
|
215
|
+
|
216
|
+
Args:
|
217
|
+
context: Deployment context
|
218
|
+
deployment_info: Deployment information
|
219
|
+
|
220
|
+
Returns:
|
221
|
+
True if deployment verified
|
222
|
+
"""
|
223
|
+
deployment_url = deployment_info.get("deployment_url")
|
224
|
+
|
225
|
+
if not deployment_url:
|
226
|
+
self._logger.error("No deployment URL to verify")
|
227
|
+
return False
|
228
|
+
|
229
|
+
# Check deployment status via API or HTTP
|
230
|
+
try:
|
231
|
+
import urllib.request
|
232
|
+
|
233
|
+
# Try to access the deployment
|
234
|
+
with urllib.request.urlopen(deployment_url) as response:
|
235
|
+
if response.status == 200:
|
236
|
+
self._logger.info(f"Deployment verified: {deployment_url}")
|
237
|
+
return True
|
238
|
+
else:
|
239
|
+
self._logger.error(f"Deployment returned status: {response.status}")
|
240
|
+
return False
|
241
|
+
|
242
|
+
except Exception as e:
|
243
|
+
self._logger.error(f"Failed to verify deployment: {str(e)}")
|
244
|
+
# May still be building, check via CLI
|
245
|
+
return self._check_deployment_status(deployment_info.get("deployment_id"))
|
246
|
+
|
247
|
+
def rollback(
|
248
|
+
self, context: DeploymentContext, result: DeploymentResult
|
249
|
+
) -> bool:
|
250
|
+
"""
|
251
|
+
Rollback Vercel deployment.
|
252
|
+
|
253
|
+
Args:
|
254
|
+
context: Deployment context
|
255
|
+
result: Current deployment result
|
256
|
+
|
257
|
+
Returns:
|
258
|
+
True if rollback successful
|
259
|
+
"""
|
260
|
+
try:
|
261
|
+
# Vercel doesn't support direct rollback via CLI
|
262
|
+
# Instead, we can promote a previous deployment
|
263
|
+
|
264
|
+
if context.config.get("previous_deployment_id"):
|
265
|
+
# Promote previous deployment
|
266
|
+
cmd = [
|
267
|
+
"vercel",
|
268
|
+
"promote",
|
269
|
+
context.config["previous_deployment_id"],
|
270
|
+
"--yes",
|
271
|
+
]
|
272
|
+
|
273
|
+
if context.config.get("token"):
|
274
|
+
cmd.extend(["--token", context.config["token"]])
|
275
|
+
|
276
|
+
result = subprocess.run(
|
277
|
+
cmd,
|
278
|
+
capture_output=True,
|
279
|
+
text=True,
|
280
|
+
check=True,
|
281
|
+
)
|
282
|
+
|
283
|
+
self._logger.info(f"Rolled back to previous deployment")
|
284
|
+
return True
|
285
|
+
|
286
|
+
else:
|
287
|
+
self._logger.warning(
|
288
|
+
"No previous deployment ID available for rollback. "
|
289
|
+
"Manual rollback required via Vercel dashboard."
|
290
|
+
)
|
291
|
+
return False
|
292
|
+
|
293
|
+
except Exception as e:
|
294
|
+
self._logger.error(f"Rollback failed: {str(e)}")
|
295
|
+
return False
|
296
|
+
|
297
|
+
def get_health_status(
|
298
|
+
self, deployment_info: Dict[str, Any]
|
299
|
+
) -> Dict[str, Any]:
|
300
|
+
"""
|
301
|
+
Get health status of Vercel deployment.
|
302
|
+
|
303
|
+
Args:
|
304
|
+
deployment_info: Deployment information
|
305
|
+
|
306
|
+
Returns:
|
307
|
+
Health status information
|
308
|
+
"""
|
309
|
+
deployment_url = deployment_info.get("deployment_url")
|
310
|
+
|
311
|
+
health = {
|
312
|
+
"status": "unknown",
|
313
|
+
"deployment_url": deployment_url,
|
314
|
+
"checks": {},
|
315
|
+
}
|
316
|
+
|
317
|
+
if not deployment_url:
|
318
|
+
health["status"] = "unhealthy"
|
319
|
+
health["error"] = "No deployment URL"
|
320
|
+
return health
|
321
|
+
|
322
|
+
try:
|
323
|
+
import urllib.request
|
324
|
+
|
325
|
+
# Check main deployment URL
|
326
|
+
with urllib.request.urlopen(deployment_url) as response:
|
327
|
+
health["checks"]["main_url"] = response.status == 200
|
328
|
+
health["response_time_ms"] = response.info().get("X-Vercel-Trace", "N/A")
|
329
|
+
|
330
|
+
# Check functions if configured
|
331
|
+
if deployment_info.get("functions"):
|
332
|
+
for func_name in deployment_info["functions"]:
|
333
|
+
func_url = f"{deployment_url}/api/{func_name}"
|
334
|
+
try:
|
335
|
+
with urllib.request.urlopen(func_url) as response:
|
336
|
+
health["checks"][f"function_{func_name}"] = response.status < 500
|
337
|
+
except:
|
338
|
+
health["checks"][f"function_{func_name}"] = False
|
339
|
+
|
340
|
+
# Determine overall status
|
341
|
+
if all(health["checks"].values()):
|
342
|
+
health["status"] = "healthy"
|
343
|
+
elif any(health["checks"].values()):
|
344
|
+
health["status"] = "degraded"
|
345
|
+
else:
|
346
|
+
health["status"] = "unhealthy"
|
347
|
+
|
348
|
+
except Exception as e:
|
349
|
+
health["status"] = "unhealthy"
|
350
|
+
health["error"] = str(e)
|
351
|
+
|
352
|
+
return health
|
353
|
+
|
354
|
+
# Private helper methods
|
355
|
+
|
356
|
+
def _check_vercel_cli(self) -> bool:
|
357
|
+
"""Check if Vercel CLI is installed."""
|
358
|
+
try:
|
359
|
+
result = subprocess.run(
|
360
|
+
["vercel", "--version"],
|
361
|
+
capture_output=True,
|
362
|
+
text=True,
|
363
|
+
check=True,
|
364
|
+
)
|
365
|
+
return True
|
366
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
367
|
+
return False
|
368
|
+
|
369
|
+
def _check_vercel_auth(self) -> bool:
|
370
|
+
"""Check if authenticated with Vercel."""
|
371
|
+
try:
|
372
|
+
result = subprocess.run(
|
373
|
+
["vercel", "whoami"],
|
374
|
+
capture_output=True,
|
375
|
+
text=True,
|
376
|
+
check=True,
|
377
|
+
)
|
378
|
+
return True
|
379
|
+
except subprocess.CalledProcessError:
|
380
|
+
return False
|
381
|
+
|
382
|
+
def _prepare_vercel_config(
|
383
|
+
self, context: DeploymentContext, deploy_dir: Path
|
384
|
+
) -> Optional[Path]:
|
385
|
+
"""Prepare vercel.json configuration."""
|
386
|
+
config = context.config
|
387
|
+
vercel_config = {}
|
388
|
+
|
389
|
+
# Add project settings
|
390
|
+
if config.get("project_name"):
|
391
|
+
vercel_config["name"] = config["project_name"]
|
392
|
+
|
393
|
+
# Add build settings
|
394
|
+
if config.get("build_command"):
|
395
|
+
vercel_config["buildCommand"] = config["build_command"]
|
396
|
+
|
397
|
+
if config.get("output_directory"):
|
398
|
+
vercel_config["outputDirectory"] = config["output_directory"]
|
399
|
+
|
400
|
+
# Add functions configuration
|
401
|
+
if config.get("functions"):
|
402
|
+
vercel_config["functions"] = config["functions"]
|
403
|
+
|
404
|
+
# Add routes/rewrites
|
405
|
+
if config.get("rewrites"):
|
406
|
+
vercel_config["rewrites"] = config["rewrites"]
|
407
|
+
|
408
|
+
if config.get("redirects"):
|
409
|
+
vercel_config["redirects"] = config["redirects"]
|
410
|
+
|
411
|
+
# Add environment configuration
|
412
|
+
if config.get("env"):
|
413
|
+
vercel_config["env"] = config["env"]
|
414
|
+
|
415
|
+
if vercel_config:
|
416
|
+
config_path = deploy_dir / "vercel.json"
|
417
|
+
with open(config_path, "w") as f:
|
418
|
+
json.dump(vercel_config, f, indent=2)
|
419
|
+
return config_path
|
420
|
+
|
421
|
+
return None
|
422
|
+
|
423
|
+
def _prepare_env_file(
|
424
|
+
self, context: DeploymentContext, deploy_dir: Path
|
425
|
+
) -> Optional[Path]:
|
426
|
+
"""Prepare environment variables file."""
|
427
|
+
env_vars = context.config.get("env", {})
|
428
|
+
|
429
|
+
if env_vars:
|
430
|
+
env_file = deploy_dir / ".env"
|
431
|
+
with open(env_file, "w") as f:
|
432
|
+
for key, value in env_vars.items():
|
433
|
+
f.write(f"{key}={value}\n")
|
434
|
+
return env_file
|
435
|
+
|
436
|
+
return None
|
437
|
+
|
438
|
+
def _parse_deployment_url(self, output: str) -> Optional[str]:
|
439
|
+
"""Parse deployment URL from Vercel output."""
|
440
|
+
# Look for URL patterns in output
|
441
|
+
lines = output.split("\n")
|
442
|
+
for line in lines:
|
443
|
+
if "https://" in line:
|
444
|
+
# Extract URL
|
445
|
+
import re
|
446
|
+
url_match = re.search(r"https://[^\s]+", line)
|
447
|
+
if url_match:
|
448
|
+
return url_match.group(0)
|
449
|
+
|
450
|
+
return None
|
451
|
+
|
452
|
+
def _check_deployment_status(self, deployment_id: str) -> bool:
|
453
|
+
"""Check deployment status via Vercel CLI."""
|
454
|
+
try:
|
455
|
+
cmd = ["vercel", "inspect", deployment_id]
|
456
|
+
result = subprocess.run(
|
457
|
+
cmd,
|
458
|
+
capture_output=True,
|
459
|
+
text=True,
|
460
|
+
check=True,
|
461
|
+
)
|
462
|
+
|
463
|
+
# Check for ready state in output
|
464
|
+
return "State: READY" in result.stdout or "ready" in result.stdout.lower()
|
465
|
+
|
466
|
+
except subprocess.CalledProcessError:
|
467
|
+
return False
|
468
|
+
|
469
|
+
def _generate_deployment_id(self) -> str:
|
470
|
+
"""Generate unique deployment ID."""
|
471
|
+
return f"vercel_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{id(self) % 10000:04d}"
|