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.
- mobox/__init__.py +1 -0
- mobox/auth.py +254 -0
- mobox/client.py +98 -0
- mobox/commands/__init__.py +0 -0
- mobox/commands/deploy.py +149 -0
- mobox/commands/local.py +82 -0
- mobox/commands/manage.py +188 -0
- mobox/commands/ops.py +125 -0
- mobox/config.py +82 -0
- mobox/local/__init__.py +0 -0
- mobox/local/analyze_volumes.py +483 -0
- mobox/local/config_utils.py +632 -0
- mobox/local/detect_framework.py +1406 -0
- mobox/local/detect_project.py +388 -0
- mobox/local/pack_project.py +543 -0
- mobox/main.py +109 -0
- mobox/utils.py +52 -0
- mobox_cli-0.1.0.dist-info/METADATA +9 -0
- mobox_cli-0.1.0.dist-info/RECORD +21 -0
- mobox_cli-0.1.0.dist-info/WHEEL +4 -0
- mobox_cli-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -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()
|