mobox-cli 0.1.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,388 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Project Detector - 一体化项目检测工具
4
+
5
+ 一次性完成:
6
+ 1. 框架检测 (detect_framework) - 支持 hybrid 类型和 MCP 特性
7
+ 2. 持久化分析 (analyze_volumes)
8
+ 3. 配置文件生成 (app-deploy.json)
9
+
10
+ 版本: v1.1.0
11
+ """
12
+
13
+ import json
14
+ import sys
15
+ from pathlib import Path
16
+ from typing import Dict
17
+
18
+ # Import existing script modules
19
+ from .detect_framework import FrameworkDetector
20
+ from .analyze_volumes import VolumeAnalyzer
21
+ from .config_utils import (
22
+ read_config, write_config, compare_detection,
23
+ merge_config, get_change_summary
24
+ )
25
+
26
+
27
+ class ProjectDetector:
28
+ def __init__(self, project_path: str):
29
+ self.project_path = Path(project_path).resolve()
30
+ self.framework_result = None
31
+ self.volumes_result = None
32
+ self.config_result = None
33
+
34
+ def detect(self, force_update: bool = False) -> Dict:
35
+ """
36
+ 执行项目检测(支持智能增量更新和 hybrid 类型)
37
+
38
+ 一次性完成:
39
+ 1. 读取现有配置(如果存在)
40
+ 2. 检测框架信息(支持 hybrid)
41
+ 3. 分析持久化卷
42
+ 4. 比对检测结果
43
+ 5. 增量更新配置文件(仅在需要时)
44
+
45
+ Args:
46
+ force_update: 强制更新配置(默认False,自动判断)
47
+
48
+ Returns:
49
+ 检测结果字典
50
+ """
51
+ # 验证项目路径
52
+ if not self.project_path.exists():
53
+ return {
54
+ 'success': False,
55
+ 'error': f'Project path does not exist: {self.project_path}'
56
+ }
57
+
58
+ # 步骤 1: 读取现有配置
59
+ try:
60
+ existing_config = read_config(str(self.project_path))
61
+ if existing_config:
62
+ app_name = existing_config.get('appName', 'N/A')
63
+ framework = existing_config.get('framework', 'N/A')
64
+ print(f"ℹ️ Found existing configuration:")
65
+ print(f" App Name: {app_name}")
66
+ print(f" Framework: {framework}")
67
+ print()
68
+ except ValueError as e:
69
+ print(f"⚠️ Warning: Invalid existing config: {e}")
70
+ existing_config = None
71
+
72
+ # 步骤 2: 检测框架
73
+ print(f"🔍 Detecting framework...")
74
+ framework_detector = FrameworkDetector(str(self.project_path))
75
+ self.framework_result = framework_detector.detect()
76
+
77
+ if not self.framework_result.get('success'):
78
+ return {
79
+ 'success': False,
80
+ 'error': 'Framework detection failed',
81
+ 'details': self.framework_result
82
+ }
83
+
84
+ framework = self.framework_result.get('framework')
85
+
86
+ # 根据框架类型输出不同信息
87
+ if framework == 'hybrid':
88
+ self._print_hybrid_info()
89
+ else:
90
+ self._print_single_framework_info()
91
+
92
+ # 步骤 3: 分析持久化需求
93
+ print(f"\n📦 Analyzing volumes...")
94
+ volumes = self._analyze_volumes()
95
+
96
+ if volumes:
97
+ print(f" ✓ Found {len(volumes)} persistent directories:")
98
+ for vol in volumes:
99
+ print(f" - {vol['source']} ({vol.get('size', 'N/A')}) - {vol.get('reason', 'N/A')}")
100
+ else:
101
+ print(f" ℹ No persistent directories detected")
102
+
103
+ # 步骤 4: 构建检测结果
104
+ detection_result = self._build_detection_result(volumes)
105
+
106
+ # 步骤 5: 比对是否需要更新
107
+ if not force_update and existing_config:
108
+ if not compare_detection(existing_config, detection_result):
109
+ print(f"\n✅ No changes detected in project structure")
110
+ print(f"ℹ️ Configuration unchanged, skip writing")
111
+
112
+ result = {
113
+ 'success': True,
114
+ 'operation': 'no_changes',
115
+ 'project_path': str(self.project_path),
116
+ 'framework': framework,
117
+ 'message': 'Project structure unchanged, configuration preserved'
118
+ }
119
+
120
+ # 包含配置信息
121
+ result['existing_config'] = {
122
+ 'appName': existing_config.get('appName'),
123
+ 'appVersion': existing_config.get('appVersion')
124
+ }
125
+
126
+ # 透传 standalone 信息 (Next.js)
127
+ standalone = self.framework_result.get('standalone')
128
+ if standalone:
129
+ result['standalone'] = standalone
130
+
131
+ return result
132
+
133
+ # 步骤 6: 增量更新配置
134
+ print(f"\n📝 Updating configuration...")
135
+ merged_config = merge_config(existing_config, detection_result, str(self.project_path))
136
+
137
+ try:
138
+ write_config(str(self.project_path), merged_config)
139
+ print(f" ✓ Config updated (user fields preserved)")
140
+
141
+ # 显示变更摘要
142
+ if existing_config:
143
+ changes = get_change_summary(existing_config, detection_result)
144
+ if changes:
145
+ print(f" 📋 Changes:")
146
+ for change in changes:
147
+ print(f" - {change}")
148
+
149
+ except ValueError as e:
150
+ return {
151
+ 'success': False,
152
+ 'error': 'CONFIG_UPDATE_FAILED',
153
+ 'message': f'Failed to update configuration: {str(e)}'
154
+ }
155
+
156
+ print(f"\n✅ Detection complete!")
157
+
158
+ result = {
159
+ 'success': True,
160
+ 'operation': 'updated' if existing_config else 'created',
161
+ 'project_path': str(self.project_path),
162
+ 'framework': framework,
163
+ 'message': 'Project detected and configured successfully'
164
+ }
165
+
166
+ # 单一框架类型包含额外信息
167
+ if framework != 'hybrid':
168
+ result['confidence'] = self.framework_result.get('confidence')
169
+ result['detection_method'] = self.framework_result.get('detection_method')
170
+
171
+ # 透传 standalone 信息 (Next.js)
172
+ standalone = self.framework_result.get('standalone')
173
+ if standalone:
174
+ result['standalone'] = standalone
175
+
176
+ # 包含配置信息
177
+ result['config'] = {
178
+ 'appName': merged_config.get('appName'),
179
+ 'appVersion': merged_config.get('appVersion')
180
+ }
181
+
182
+ return result
183
+
184
+ def _print_hybrid_info(self):
185
+ """输出 hybrid 类型信息"""
186
+ print(f" ✓ Framework: hybrid (mixed project)")
187
+
188
+ # 输出组件表格
189
+ components = self.framework_result.get('components', [])
190
+ print(f"\n 📦 Components:")
191
+ print(f" ┌{'─'*12}┬{'─'*10}┬{'─'*12}┬{'─'*7}┬{'─'*9}┐")
192
+ print(f" │{'Path':^12}│{'Language':^10}│{'Framework':^12}│{'Port':^7}│{'Primary':^9}│")
193
+ print(f" ├{'─'*12}┼{'─'*10}┼{'─'*12}┼{'─'*7}┼{'─'*9}┤")
194
+ for comp in components:
195
+ path = comp.get('path', '.')[:10]
196
+ lang = comp.get('language', '')[:8]
197
+ fw = comp.get('framework', '')[:10]
198
+ port = str(comp.get('port', ''))[:5]
199
+ primary = '✓' if comp.get('primary') else ''
200
+ print(f" │{path:^12}│{lang:^10}│{fw:^12}│{port:^7}│{primary:^9}│")
201
+ print(f" └{'─'*12}┴{'─'*10}┴{'─'*12}┴{'─'*7}┴{'─'*9}┘")
202
+
203
+ # 输出运行时信息
204
+ runtime = self.framework_result.get('runtime', {})
205
+ print(f"\n Runtime:")
206
+ print(f" Command: {runtime.get('command', 'N/A')}")
207
+ print(f" Requires: {', '.join(runtime.get('requires', []))}")
208
+
209
+ # 输出 features
210
+ features = self.framework_result.get('features', [])
211
+ if features:
212
+ print(f" Features: {', '.join(features)}")
213
+
214
+ # 输出 MCP 警告
215
+ for comp in components:
216
+ mcp = comp.get('mcp', {})
217
+ if mcp.get('warning'):
218
+ print(f"\n ⚠️ MCP Warning: {mcp['warning']}")
219
+
220
+ def _print_single_framework_info(self):
221
+ """输出单一框架类型信息"""
222
+ framework = self.framework_result.get('framework')
223
+ port = self.framework_result.get('port')
224
+ start_command = self.framework_result.get('start_command')
225
+ start_command_hint = self.framework_result.get('start_command_hint')
226
+
227
+ print(f" ✓ Framework: {framework}")
228
+ print(f" ✓ Port: {port}")
229
+ print(f" ✓ Build: {self.framework_result.get('build_command')}")
230
+
231
+ # 输出 features
232
+ features = self.framework_result.get('features', [])
233
+ if features:
234
+ print(f" ✓ Features: {', '.join(features)}")
235
+
236
+ # 输出 MCP 信息
237
+ mcp = self.framework_result.get('mcp', {})
238
+ if mcp:
239
+ print(f" ✓ MCP Transport: {mcp.get('transport', 'N/A')}")
240
+ if mcp.get('warning'):
241
+ print(f" ⚠️ MCP Warning: {mcp['warning']}")
242
+
243
+ # Show warning if start_command is missing
244
+ if start_command:
245
+ print(f" ✓ Start: {start_command}")
246
+ else:
247
+ print(f" ⚠️ Start: Not detected")
248
+ if start_command_hint:
249
+ # Print hint with indentation
250
+ print(f"\n 💡 Suggestion:")
251
+ for line in start_command_hint.split('\n'):
252
+ print(f" {line}")
253
+
254
+ # Next.js Standalone 模式建议
255
+ standalone = self.framework_result.get('standalone', {})
256
+ if standalone:
257
+ if standalone.get('enabled'):
258
+ print(f" ✓ Standalone: enabled")
259
+ elif standalone.get('shouldSuggest'):
260
+ print(f"\n 💡 Standalone Mode Suggestion:")
261
+ print(f" Standalone mode can reduce image size by 75% (900MB → 225MB)")
262
+ print(f" Config file: {standalone.get('configFile', 'next.config.js')}")
263
+ elif standalone.get('skipReason'):
264
+ reason = standalone.get('skipReason')
265
+ if reason == 'custom_server':
266
+ print(f" ℹ️ Standalone: skipped (custom server detected)")
267
+ elif reason == 'version_incompatible':
268
+ print(f" ℹ️ Standalone: skipped (requires Next.js >= 12.0)")
269
+ elif reason == 'other_output_mode':
270
+ print(f" ℹ️ Standalone: skipped (other output mode configured)")
271
+
272
+ def _analyze_volumes(self) -> list:
273
+ """分析持久化卷(支持 hybrid 多路径)"""
274
+ framework = self.framework_result.get('framework')
275
+
276
+ if framework == 'hybrid':
277
+ # hybrid 项目:扫描所有组件路径
278
+ all_volumes = []
279
+ components = self.framework_result.get('components', [])
280
+
281
+ for comp in components:
282
+ comp_path = comp.get('path', '.')
283
+ if comp_path == '.':
284
+ full_path = self.project_path
285
+ else:
286
+ full_path = self.project_path / comp_path.lstrip('./')
287
+
288
+ volume_analyzer = VolumeAnalyzer(str(full_path))
289
+ result = volume_analyzer.analyze()
290
+
291
+ if result.get('success'):
292
+ for vol in result.get('suggested', []):
293
+ # 调整路径前缀
294
+ if comp_path != '.':
295
+ vol['source'] = f"{comp_path}/{vol['source'].lstrip('./')}"
296
+ all_volumes.append(vol)
297
+
298
+ return all_volumes
299
+ else:
300
+ # 单一项目:原有逻辑
301
+ volume_analyzer = VolumeAnalyzer(str(self.project_path))
302
+ self.volumes_result = volume_analyzer.analyze()
303
+
304
+ if not self.volumes_result.get('success'):
305
+ return []
306
+
307
+ return self.volumes_result.get('suggested', [])
308
+
309
+ def _build_detection_result(self, volumes: list) -> Dict:
310
+ """构建检测结果(支持 hybrid 类型)"""
311
+ framework = self.framework_result.get('framework')
312
+
313
+ if framework == 'hybrid':
314
+ # hybrid 类型配置
315
+ result = {
316
+ 'framework': 'hybrid',
317
+ 'components': self.framework_result.get('components', []),
318
+ 'runtime': self.framework_result.get('runtime', {}),
319
+ 'volumes': volumes
320
+ }
321
+
322
+ # 添加 features
323
+ features = self.framework_result.get('features', [])
324
+ if features:
325
+ result['features'] = features
326
+
327
+ return result
328
+ else:
329
+ # 单一框架类型配置
330
+ result = {
331
+ 'framework': framework,
332
+ 'build': {
333
+ 'command': self.framework_result.get('build_command', ""),
334
+ 'timeout': 600
335
+ },
336
+ 'runtime': {
337
+ 'command': self.framework_result.get('start_command'),
338
+ 'port': self.framework_result.get('port'),
339
+ 'nodeVersion': self.framework_result.get('node_version', "")
340
+ },
341
+ 'volumes': volumes
342
+ }
343
+
344
+ # 添加 features
345
+ features = self.framework_result.get('features', [])
346
+ if features:
347
+ result['features'] = features
348
+
349
+ # 添加 mcp 配置
350
+ mcp = self.framework_result.get('mcp')
351
+ if mcp:
352
+ result['mcp'] = mcp
353
+
354
+ # 添加 standalone 配置 (Next.js 专属)
355
+ standalone = self.framework_result.get('standalone')
356
+ if standalone:
357
+ result['standalone'] = standalone
358
+
359
+ return result
360
+
361
+
362
+ def main():
363
+ if len(sys.argv) < 2:
364
+ print(json.dumps({
365
+ 'success': False,
366
+ 'error': 'Usage: detect_project.py <project_path>',
367
+ 'example': 'detect_project.py /path/to/project'
368
+ }, indent=2))
369
+ sys.exit(1)
370
+
371
+ project_path = sys.argv[1]
372
+
373
+ # 执行检测
374
+ detector = ProjectDetector(project_path)
375
+ result = detector.detect()
376
+
377
+ # 输出 JSON 结果(用于程序化调用)
378
+ print("\n" + "="*60)
379
+ print("JSON Result:")
380
+ print("="*60)
381
+ print(json.dumps(result, indent=2))
382
+
383
+ if not result.get('success', False):
384
+ sys.exit(1)
385
+
386
+
387
+ if __name__ == '__main__':
388
+ main()