x-ipe 1.0.23__py3-none-any.whl → 1.0.25__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.
- x_ipe/app.py +32 -1
- x_ipe/handlers/terminal_handlers.py +6 -0
- x_ipe/handlers/voice_handlers.py +5 -0
- x_ipe/resources/copilot-instructions.md +19 -6
- x_ipe/resources/skills/lesson-learned/SKILL.md +208 -0
- x_ipe/resources/skills/lesson-learned/references/examples.md +238 -0
- x_ipe/resources/skills/project-quality-board-management/SKILL.md +135 -298
- x_ipe/resources/skills/project-quality-board-management/references/evaluation-principles.md +213 -0
- x_ipe/resources/skills/project-quality-board-management/references/evaluation-procedures.md +214 -0
- x_ipe/resources/skills/project-quality-board-management/templates/quality-report.md +70 -18
- x_ipe/resources/skills/task-execution-guideline/SKILL.md +2 -2
- x_ipe/resources/skills/task-execution-guideline/templates/task-record.yaml +1 -1
- x_ipe/resources/skills/task-type-code-implementation/SKILL.md +72 -270
- x_ipe/resources/skills/task-type-code-implementation/references/implementation-guidelines.md +432 -0
- x_ipe/resources/skills/task-type-code-refactor-v2/SKILL.md +127 -353
- x_ipe/resources/skills/task-type-code-refactor-v2/references/refactoring-techniques.md +373 -0
- x_ipe/resources/skills/task-type-feature-breakdown/SKILL.md +31 -243
- x_ipe/resources/skills/task-type-feature-breakdown/references/breakdown-guidelines.md +330 -0
- x_ipe/resources/skills/task-type-feature-refinement/SKILL.md +27 -180
- x_ipe/resources/skills/task-type-feature-refinement/references/specification-writing-guide.md +267 -0
- x_ipe/resources/skills/task-type-idea-mockup/SKILL.md +38 -276
- x_ipe/resources/skills/task-type-idea-mockup/references/mockup-guidelines.md +299 -0
- x_ipe/resources/skills/task-type-idea-to-architecture/SKILL.md +20 -218
- x_ipe/resources/skills/task-type-idea-to-architecture/references/architecture-patterns.md +342 -0
- x_ipe/resources/skills/task-type-ideation/SKILL.md +10 -266
- x_ipe/resources/skills/task-type-ideation/references/folder-naming-guide.md +55 -0
- x_ipe/resources/skills/task-type-ideation/references/tool-usage-guide.md +236 -0
- x_ipe/resources/skills/task-type-ideation-v2/SKILL.md +488 -0
- x_ipe/resources/skills/task-type-ideation-v2/references/examples.md +377 -0
- x_ipe/resources/skills/task-type-ideation-v2/references/folder-naming-guide.md +74 -0
- x_ipe/resources/skills/task-type-ideation-v2/references/tool-usage-guide.md +145 -0
- x_ipe/resources/skills/task-type-ideation-v2/references/visualization-guide.md +160 -0
- x_ipe/resources/skills/task-type-ideation-v2/templates/idea-summary.md +86 -0
- x_ipe/resources/skills/task-type-refactoring-analysis/SKILL.md +83 -145
- x_ipe/resources/skills/task-type-refactoring-analysis/references/output-schema.md +172 -0
- x_ipe/resources/skills/task-type-technical-design/SKILL.md +28 -214
- x_ipe/resources/skills/task-type-technical-design/references/design-templates.md +422 -0
- x_ipe/resources/skills/task-type-test-generation/SKILL.md +47 -332
- x_ipe/resources/skills/task-type-test-generation/references/test-patterns.md +368 -0
- x_ipe/resources/skills/tool-tracing-creator/SKILL.md +312 -0
- x_ipe/resources/skills/tool-tracing-creator/references/examples.md +324 -0
- x_ipe/resources/skills/tool-tracing-instrumentation/SKILL.md +373 -0
- x_ipe/resources/skills/tool-tracing-instrumentation/references/examples.md +264 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/SKILL.md +486 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/10. example-gate-conditions.md +73 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/11. reference-quality-standards.md +127 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/2. reference-section-order.md +127 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/3. example-step-based-code-review.md +84 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/4. example-step-based-feature-implementation.md +113 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/5. example-function-based-validation.md +73 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/6. example-function-based-analysis.md +94 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/7. example-task-io-code-implementation.md +36 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/8. example-structured-summary.md +43 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/9. example-dor-dod.md +77 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/examples.md +429 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/skill-general-guidelines-v2.md +611 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/skill-meta-x-ipe-meta.md +153 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/skill-meta-x-ipe-task-based.md +324 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/skill-meta-x-ipe-task-category.md +109 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/skill-meta-x-ipe-tool.md +205 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/x-ipe-meta.md +334 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/x-ipe-task-based.md +279 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/x-ipe-tool.md +175 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/x-ipe-workflow-orchestration.md +329 -0
- x_ipe/resources/skills/x-ipe-task-based-ideation/SKILL.md +487 -0
- x_ipe/resources/skills/x-ipe-task-based-ideation/references/examples.md +377 -0
- x_ipe/resources/skills/x-ipe-task-based-ideation/references/folder-naming-guide.md +74 -0
- x_ipe/resources/skills/x-ipe-task-based-ideation/references/tool-usage-guide.md +145 -0
- x_ipe/resources/skills/x-ipe-task-based-ideation/references/visualization-guide.md +160 -0
- x_ipe/resources/skills/x-ipe-task-based-ideation/templates/idea-summary.md +86 -0
- x_ipe/routes/__init__.py +2 -0
- x_ipe/routes/ideas_routes.py +289 -0
- x_ipe/routes/kb_routes.py +80 -0
- x_ipe/routes/main_routes.py +18 -0
- x_ipe/routes/project_routes.py +7 -0
- x_ipe/routes/proxy_routes.py +10 -2
- x_ipe/routes/quality_evaluation_routes.py +193 -0
- x_ipe/routes/settings_routes.py +6 -0
- x_ipe/routes/tools_routes.py +6 -0
- x_ipe/routes/tracing_routes.py +232 -0
- x_ipe/routes/uiux_feedback_routes.py +50 -0
- x_ipe/services/__init__.py +5 -0
- x_ipe/services/config_service.py +6 -0
- x_ipe/services/file_service.py +20 -0
- x_ipe/services/homepage_service.py +160 -0
- x_ipe/services/ideas_service.py +535 -2
- x_ipe/services/kb_service.py +378 -0
- x_ipe/services/proxy_service.py +37 -7
- x_ipe/services/settings_service.py +13 -0
- x_ipe/services/skills_service.py +4 -0
- x_ipe/services/terminal_service.py +24 -0
- x_ipe/services/themes_service.py +4 -0
- x_ipe/services/tools_config_service.py +4 -0
- x_ipe/services/tracing_service.py +333 -0
- x_ipe/services/uiux_feedback_service.py +148 -1
- x_ipe/services/voice_input_service_v2.py +11 -0
- x_ipe/static/css/base.css +7 -0
- x_ipe/static/css/homepage-infinity.css +330 -0
- x_ipe/static/css/kb-core.css +301 -0
- x_ipe/static/css/quality-evaluation.css +345 -0
- x_ipe/static/css/sidebar.css +14 -4
- x_ipe/static/css/terminal.css +23 -0
- x_ipe/static/css/tracing-dashboard.css +796 -0
- x_ipe/static/css/uiux-feedback.css +7 -1
- x_ipe/static/css/workplace.css +636 -0
- x_ipe/static/img/homepage-infinity-loop.png +0 -0
- x_ipe/static/js/features/confirm-dialog.js +169 -0
- x_ipe/static/js/features/folder-view.js +742 -0
- x_ipe/static/js/features/homepage-infinity.js +314 -0
- x_ipe/static/js/features/kb-core.js +371 -0
- x_ipe/static/js/features/quality-evaluation.js +387 -0
- x_ipe/static/js/features/sidebar.js +255 -12
- x_ipe/static/js/features/tracing-dashboard.js +855 -0
- x_ipe/static/js/features/tracing-graph.js +1031 -0
- x_ipe/static/js/features/tree-drag.js +227 -0
- x_ipe/static/js/features/tree-search.js +228 -0
- x_ipe/static/js/features/workplace.js +661 -33
- x_ipe/static/js/init.js +76 -0
- x_ipe/static/js/terminal-v2.js +45 -14
- x_ipe/static/js/terminal.js +50 -49
- x_ipe/static/js/uiux-feedback.js +75 -16
- x_ipe/templates/base.html +24 -0
- x_ipe/templates/index.html +10 -1
- x_ipe/templates/knowledge-base.html +110 -0
- x_ipe/templates/workplace.html +4 -0
- x_ipe/tracing/__init__.py +37 -0
- x_ipe/tracing/buffer.py +135 -0
- x_ipe/tracing/context.py +125 -0
- x_ipe/tracing/decorator.py +288 -0
- x_ipe/tracing/middleware.py +197 -0
- x_ipe/tracing/parser.py +235 -0
- x_ipe/tracing/redactor.py +111 -0
- x_ipe/tracing/writer.py +122 -0
- {x_ipe-1.0.23.dist-info → x_ipe-1.0.25.dist-info}/METADATA +2 -2
- {x_ipe-1.0.23.dist-info → x_ipe-1.0.25.dist-info}/RECORD +138 -65
- x_ipe/app.py.bak +0 -1333
- x_ipe/resources/skills/x-ipe-skill-creator/SKILL.md +0 -329
- x_ipe/resources/skills/x-ipe-skill-creator/references/output-patterns.md +0 -169
- x_ipe/resources/skills/x-ipe-skill-creator/references/skill-structure.md +0 -162
- x_ipe/resources/skills/x-ipe-skill-creator/references/workflows.md +0 -110
- x_ipe/resources/skills/x-ipe-skill-creator/templates/references/examples.md +0 -113
- x_ipe/resources/skills/x-ipe-skill-creator/templates/skill-category-skill.md +0 -296
- x_ipe/resources/skills/x-ipe-skill-creator/templates/task-type-skill.md +0 -269
- {x_ipe-1.0.23.dist-info → x_ipe-1.0.25.dist-info}/WHEEL +0 -0
- {x_ipe-1.0.23.dist-info → x_ipe-1.0.25.dist-info}/entry_points.txt +0 -0
- {x_ipe-1.0.23.dist-info → x_ipe-1.0.25.dist-info}/licenses/LICENSE +0 -0
x_ipe/app.py.bak
DELETED
|
@@ -1,1333 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Flask Application for Document Viewer
|
|
3
|
-
|
|
4
|
-
FEATURE-001: Project Navigation
|
|
5
|
-
- Provides API for project structure
|
|
6
|
-
- HTTP polling for real-time updates (no WebSocket)
|
|
7
|
-
- Serves frontend with sidebar navigation
|
|
8
|
-
|
|
9
|
-
FEATURE-005: Interactive Console v4.0
|
|
10
|
-
- WebSocket handlers for terminal I/O
|
|
11
|
-
- Session persistence with automatic reconnection
|
|
12
|
-
- xterm.js frontend integration
|
|
13
|
-
"""
|
|
14
|
-
import os
|
|
15
|
-
import sys
|
|
16
|
-
import json
|
|
17
|
-
from pathlib import Path
|
|
18
|
-
from flask import Flask, render_template, jsonify, request, current_app, send_file
|
|
19
|
-
from flask_socketio import SocketIO, emit
|
|
20
|
-
|
|
21
|
-
# Load .env from config folder if it exists
|
|
22
|
-
def load_env_file():
|
|
23
|
-
"""Load environment variables from x-ipe-docs/config/.env file."""
|
|
24
|
-
env_path = Path(__file__).parent.parent.parent / 'x-ipe-docs' / 'config' / '.env'
|
|
25
|
-
if env_path.exists():
|
|
26
|
-
with open(env_path) as f:
|
|
27
|
-
for line in f:
|
|
28
|
-
line = line.strip()
|
|
29
|
-
if line and not line.startswith('#') and '=' in line:
|
|
30
|
-
key, value = line.split('=', 1)
|
|
31
|
-
key = key.strip()
|
|
32
|
-
value = value.strip()
|
|
33
|
-
if value and key not in os.environ: # Don't override existing env vars
|
|
34
|
-
os.environ[key] = value
|
|
35
|
-
|
|
36
|
-
load_env_file()
|
|
37
|
-
|
|
38
|
-
from x_ipe.services import ProjectService, ContentService, SettingsService, ProjectFoldersService, IdeasService, ConfigService, SkillsService, ToolsConfigService, ThemesService
|
|
39
|
-
from x_ipe.services import session_manager
|
|
40
|
-
from x_ipe.config import config_by_name
|
|
41
|
-
|
|
42
|
-
# Global settings service instance
|
|
43
|
-
settings_service = None
|
|
44
|
-
|
|
45
|
-
# Global ideas service instance (FEATURE-008)
|
|
46
|
-
ideas_service = None
|
|
47
|
-
|
|
48
|
-
# Global project folders service instance (FEATURE-006 v2.0)
|
|
49
|
-
project_folders_service = None
|
|
50
|
-
|
|
51
|
-
# Global config service instance (FEATURE-010)
|
|
52
|
-
config_service = None
|
|
53
|
-
|
|
54
|
-
# Global tools config service instance (FEATURE-011)
|
|
55
|
-
tools_config_service = None
|
|
56
|
-
|
|
57
|
-
# Socket.IO instance with ping/pong for keep-alive
|
|
58
|
-
# Increased timeouts for stability - session stays open for 1 hour regardless of focus
|
|
59
|
-
socketio = SocketIO(
|
|
60
|
-
cors_allowed_origins="*",
|
|
61
|
-
async_mode='threading',
|
|
62
|
-
ping_timeout=300, # Wait 5 minutes for pong response (was 60s)
|
|
63
|
-
ping_interval=60, # Send ping every 60s (was 25s)
|
|
64
|
-
max_http_buffer_size=1e8, # 100MB max message size
|
|
65
|
-
always_connect=True, # Always emit connect even on reconnect
|
|
66
|
-
logger=False,
|
|
67
|
-
engineio_logger=False,
|
|
68
|
-
# Improved stability settings
|
|
69
|
-
http_compression=True, # Compress HTTP responses
|
|
70
|
-
manage_session=True, # Let Socket.IO manage sessions
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
# Socket SID to Session ID mapping
|
|
74
|
-
socket_to_session = {}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def create_app(config=None):
|
|
78
|
-
"""
|
|
79
|
-
Application factory for creating Flask app.
|
|
80
|
-
|
|
81
|
-
Args:
|
|
82
|
-
config: Configuration dict or config class name
|
|
83
|
-
"""
|
|
84
|
-
app = Flask(__name__,
|
|
85
|
-
static_folder='static',
|
|
86
|
-
template_folder='templates')
|
|
87
|
-
|
|
88
|
-
# Load configuration
|
|
89
|
-
if config is None:
|
|
90
|
-
config_name = os.environ.get('FLASK_ENV', 'development')
|
|
91
|
-
app.config.from_object(config_by_name.get(config_name, config_by_name['default']))
|
|
92
|
-
elif isinstance(config, dict):
|
|
93
|
-
app.config.update(config)
|
|
94
|
-
else:
|
|
95
|
-
app.config.from_object(config)
|
|
96
|
-
|
|
97
|
-
# Initialize settings service
|
|
98
|
-
global settings_service, project_folders_service, ideas_service, config_service, tools_config_service
|
|
99
|
-
db_path = app.config.get('SETTINGS_DB_PATH', app.config.get('SETTINGS_DB', os.path.join(app.instance_path, 'settings.db')))
|
|
100
|
-
settings_service = SettingsService(db_path)
|
|
101
|
-
project_folders_service = ProjectFoldersService(db_path)
|
|
102
|
-
|
|
103
|
-
# Initialize config service and load .x-ipe.yaml (FEATURE-010)
|
|
104
|
-
if not app.config.get('TESTING'):
|
|
105
|
-
config_service = ConfigService()
|
|
106
|
-
config_data = config_service.load()
|
|
107
|
-
if config_data:
|
|
108
|
-
app.config['X_IPE_CONFIG'] = config_data
|
|
109
|
-
# Always use config's project_root when .x-ipe.yaml is detected
|
|
110
|
-
app.config['PROJECT_ROOT'] = config_data.get_file_tree_path()
|
|
111
|
-
|
|
112
|
-
# Apply project_root from settings only if no .x-ipe.yaml detected
|
|
113
|
-
if not app.config.get('X_IPE_CONFIG'):
|
|
114
|
-
saved_root = settings_service.get('project_root')
|
|
115
|
-
if saved_root and saved_root != '.' and not app.config.get('TESTING'):
|
|
116
|
-
# Only apply if it's a valid path
|
|
117
|
-
if os.path.exists(saved_root) and os.path.isdir(saved_root):
|
|
118
|
-
app.config['PROJECT_ROOT'] = saved_root
|
|
119
|
-
|
|
120
|
-
# Register routes
|
|
121
|
-
register_routes(app)
|
|
122
|
-
register_settings_routes(app)
|
|
123
|
-
register_project_routes(app)
|
|
124
|
-
register_ideas_routes(app) # FEATURE-008
|
|
125
|
-
register_tools_config_routes(app) # FEATURE-011
|
|
126
|
-
|
|
127
|
-
# Initialize Socket.IO with the app
|
|
128
|
-
socketio.init_app(app)
|
|
129
|
-
|
|
130
|
-
# Register WebSocket handlers for terminal
|
|
131
|
-
register_terminal_handlers()
|
|
132
|
-
|
|
133
|
-
# Register WebSocket handlers for voice input (FEATURE-021)
|
|
134
|
-
register_voice_handlers()
|
|
135
|
-
|
|
136
|
-
return app
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
def register_routes(app):
|
|
140
|
-
"""Register all application routes"""
|
|
141
|
-
|
|
142
|
-
@app.route('/')
|
|
143
|
-
def index():
|
|
144
|
-
"""Serve main page with sidebar navigation"""
|
|
145
|
-
return render_template('index.html')
|
|
146
|
-
|
|
147
|
-
@app.route('/uiux-feedbacks')
|
|
148
|
-
def uiux_feedbacks():
|
|
149
|
-
"""
|
|
150
|
-
GET /uiux-feedbacks
|
|
151
|
-
|
|
152
|
-
FEATURE-022: UIUX Feedbacks placeholder page (CR-004)
|
|
153
|
-
Shows Work in Progress banner.
|
|
154
|
-
"""
|
|
155
|
-
return render_template('uiux-feedbacks.html')
|
|
156
|
-
|
|
157
|
-
@app.route('/workplace')
|
|
158
|
-
def workplace():
|
|
159
|
-
"""
|
|
160
|
-
GET /workplace
|
|
161
|
-
|
|
162
|
-
FEATURE-008: Workplace/Ideation page
|
|
163
|
-
CR-004: Route preserved for backward compatibility, page renamed to Ideation.
|
|
164
|
-
"""
|
|
165
|
-
return render_template('workplace.html')
|
|
166
|
-
|
|
167
|
-
@app.route('/api/project/structure')
|
|
168
|
-
def get_project_structure():
|
|
169
|
-
"""
|
|
170
|
-
GET /api/project/structure
|
|
171
|
-
|
|
172
|
-
Returns the project folder structure for sidebar navigation.
|
|
173
|
-
"""
|
|
174
|
-
project_root = app.config.get('PROJECT_ROOT')
|
|
175
|
-
|
|
176
|
-
if not project_root or not os.path.exists(project_root):
|
|
177
|
-
return jsonify({
|
|
178
|
-
'error': 'Project root not configured or does not exist',
|
|
179
|
-
'project_root': project_root
|
|
180
|
-
}), 400
|
|
181
|
-
|
|
182
|
-
service = ProjectService(project_root)
|
|
183
|
-
structure = service.get_structure()
|
|
184
|
-
|
|
185
|
-
return jsonify(structure)
|
|
186
|
-
|
|
187
|
-
@app.route('/api/file/content')
|
|
188
|
-
def get_file_content():
|
|
189
|
-
"""
|
|
190
|
-
GET /api/file/content?path=<relative_path>&raw=<true/false>
|
|
191
|
-
|
|
192
|
-
Returns the content of a file with metadata for rendering.
|
|
193
|
-
If raw=true or file is binary (images, etc.), serves the raw file content.
|
|
194
|
-
"""
|
|
195
|
-
from pathlib import Path
|
|
196
|
-
|
|
197
|
-
file_path = request.args.get('path')
|
|
198
|
-
raw = request.args.get('raw', 'false').lower() == 'true'
|
|
199
|
-
|
|
200
|
-
if not file_path:
|
|
201
|
-
return jsonify({'error': 'Path parameter required'}), 400
|
|
202
|
-
|
|
203
|
-
project_root = app.config.get('PROJECT_ROOT')
|
|
204
|
-
|
|
205
|
-
# Binary file extensions that should always be served raw
|
|
206
|
-
binary_extensions = {
|
|
207
|
-
'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.svg', '.webp',
|
|
208
|
-
'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
|
|
209
|
-
'.zip', '.rar', '.tar', '.gz', '.7z',
|
|
210
|
-
'.mp3', '.mp4', '.wav', '.avi', '.mov',
|
|
211
|
-
'.exe', '.dll', '.so', '.dylib',
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
try:
|
|
215
|
-
# Resolve path
|
|
216
|
-
full_path = (Path(project_root) / file_path).resolve()
|
|
217
|
-
|
|
218
|
-
# Security check: ensure path is within project root
|
|
219
|
-
if not str(full_path).startswith(str(Path(project_root).resolve())):
|
|
220
|
-
return jsonify({'error': 'Access denied'}), 403
|
|
221
|
-
|
|
222
|
-
if not full_path.exists():
|
|
223
|
-
return jsonify({'error': 'File not found'}), 404
|
|
224
|
-
|
|
225
|
-
ext = full_path.suffix.lower()
|
|
226
|
-
|
|
227
|
-
# Auto-detect binary files or use raw parameter
|
|
228
|
-
if raw or ext in binary_extensions:
|
|
229
|
-
# Determine MIME type
|
|
230
|
-
mime_types = {
|
|
231
|
-
'.png': 'image/png',
|
|
232
|
-
'.jpg': 'image/jpeg',
|
|
233
|
-
'.jpeg': 'image/jpeg',
|
|
234
|
-
'.gif': 'image/gif',
|
|
235
|
-
'.bmp': 'image/bmp',
|
|
236
|
-
'.ico': 'image/x-icon',
|
|
237
|
-
'.svg': 'image/svg+xml',
|
|
238
|
-
'.webp': 'image/webp',
|
|
239
|
-
'.pdf': 'application/pdf',
|
|
240
|
-
'.doc': 'application/msword',
|
|
241
|
-
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
242
|
-
'.xls': 'application/vnd.ms-excel',
|
|
243
|
-
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
244
|
-
'.ppt': 'application/vnd.ms-powerpoint',
|
|
245
|
-
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
246
|
-
'.zip': 'application/zip',
|
|
247
|
-
'.rar': 'application/vnd.rar',
|
|
248
|
-
}
|
|
249
|
-
mime_type = mime_types.get(ext, 'application/octet-stream')
|
|
250
|
-
|
|
251
|
-
return send_file(full_path, mimetype=mime_type)
|
|
252
|
-
|
|
253
|
-
service = ContentService(project_root)
|
|
254
|
-
result = service.get_content(file_path)
|
|
255
|
-
return jsonify(result)
|
|
256
|
-
except FileNotFoundError:
|
|
257
|
-
return jsonify({'error': 'File not found'}), 404
|
|
258
|
-
except PermissionError:
|
|
259
|
-
return jsonify({'error': 'Access denied'}), 403
|
|
260
|
-
except Exception as e:
|
|
261
|
-
return jsonify({'error': str(e)}), 500
|
|
262
|
-
|
|
263
|
-
@app.route('/api/file/save', methods=['POST'])
|
|
264
|
-
def save_file():
|
|
265
|
-
"""
|
|
266
|
-
POST /api/file/save
|
|
267
|
-
|
|
268
|
-
Save content to a file. Request body: {path: string, content: string}
|
|
269
|
-
|
|
270
|
-
FEATURE-003: Content Editor
|
|
271
|
-
"""
|
|
272
|
-
# Check for JSON body
|
|
273
|
-
if not request.is_json:
|
|
274
|
-
return jsonify({'success': False, 'error': 'JSON body required'}), 400
|
|
275
|
-
|
|
276
|
-
data = request.get_json()
|
|
277
|
-
|
|
278
|
-
# Validate required fields
|
|
279
|
-
if not data:
|
|
280
|
-
return jsonify({'success': False, 'error': 'Request body required'}), 400
|
|
281
|
-
|
|
282
|
-
if 'path' not in data:
|
|
283
|
-
return jsonify({'success': False, 'error': 'Path is required'}), 400
|
|
284
|
-
|
|
285
|
-
if 'content' not in data:
|
|
286
|
-
return jsonify({'success': False, 'error': 'Content is required'}), 400
|
|
287
|
-
|
|
288
|
-
project_root = app.config.get('PROJECT_ROOT')
|
|
289
|
-
|
|
290
|
-
if not project_root or not os.path.exists(project_root):
|
|
291
|
-
return jsonify({'success': False, 'error': 'Project root not configured'}), 400
|
|
292
|
-
|
|
293
|
-
service = ContentService(project_root)
|
|
294
|
-
result = service.save_content(data['path'], data['content'])
|
|
295
|
-
|
|
296
|
-
if result['success']:
|
|
297
|
-
return jsonify(result), 200
|
|
298
|
-
else:
|
|
299
|
-
return jsonify(result), 400
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
# =============================================================================
|
|
303
|
-
# FEATURE-005: Interactive Console v4.0 - WebSocket Handlers
|
|
304
|
-
# =============================================================================
|
|
305
|
-
|
|
306
|
-
def register_terminal_handlers():
|
|
307
|
-
"""Register WebSocket event handlers for terminal."""
|
|
308
|
-
|
|
309
|
-
@socketio.on('connect')
|
|
310
|
-
def handle_connect():
|
|
311
|
-
"""Handle new WebSocket connection."""
|
|
312
|
-
sid = request.sid
|
|
313
|
-
print(f"[Terminal] Client connected: {sid}")
|
|
314
|
-
|
|
315
|
-
@socketio.on('attach')
|
|
316
|
-
def handle_attach(data):
|
|
317
|
-
"""
|
|
318
|
-
Handle session attachment.
|
|
319
|
-
Creates new session or reconnects to existing one.
|
|
320
|
-
"""
|
|
321
|
-
try:
|
|
322
|
-
sid = request.sid
|
|
323
|
-
requested_session_id = data.get('session_id') if data else None
|
|
324
|
-
# Ensure rows/cols are valid integers with defaults
|
|
325
|
-
rows = data.get('rows') if data else None
|
|
326
|
-
cols = data.get('cols') if data else None
|
|
327
|
-
rows = int(rows) if rows is not None else 24
|
|
328
|
-
cols = int(cols) if cols is not None else 80
|
|
329
|
-
|
|
330
|
-
def emit_output(output_data):
|
|
331
|
-
socketio.emit('output', output_data, room=sid)
|
|
332
|
-
|
|
333
|
-
# Try to reconnect to existing session
|
|
334
|
-
if requested_session_id and session_manager.has_session(requested_session_id):
|
|
335
|
-
session = session_manager.get_session(requested_session_id)
|
|
336
|
-
|
|
337
|
-
if session.is_expired():
|
|
338
|
-
session_manager.remove_session(requested_session_id)
|
|
339
|
-
else:
|
|
340
|
-
# Reconnect to existing session
|
|
341
|
-
session.attach(sid, emit_output)
|
|
342
|
-
socket_to_session[sid] = requested_session_id
|
|
343
|
-
|
|
344
|
-
# Replay buffered output
|
|
345
|
-
buffer = session.get_buffer()
|
|
346
|
-
if buffer:
|
|
347
|
-
socketio.emit('output', buffer, room=sid)
|
|
348
|
-
|
|
349
|
-
socketio.emit('reconnected', {'session_id': requested_session_id}, room=sid)
|
|
350
|
-
return
|
|
351
|
-
|
|
352
|
-
# Create new session
|
|
353
|
-
session_id = session_manager.create_session(emit_output, rows, cols)
|
|
354
|
-
session = session_manager.get_session(session_id)
|
|
355
|
-
session.attach(sid, emit_output)
|
|
356
|
-
socket_to_session[sid] = session_id
|
|
357
|
-
|
|
358
|
-
socketio.emit('session_id', session_id, room=sid)
|
|
359
|
-
socketio.emit('new_session', {'session_id': session_id}, room=sid)
|
|
360
|
-
except Exception as e:
|
|
361
|
-
print(f"[Terminal] Error in attach handler: {e}")
|
|
362
|
-
socketio.emit('error', {'message': 'Failed to attach terminal session'}, room=sid)
|
|
363
|
-
|
|
364
|
-
@socketio.on('disconnect')
|
|
365
|
-
def handle_disconnect():
|
|
366
|
-
"""Handle WebSocket disconnection - keep session alive."""
|
|
367
|
-
try:
|
|
368
|
-
sid = request.sid
|
|
369
|
-
session_id = socket_to_session.pop(sid, None)
|
|
370
|
-
|
|
371
|
-
if session_id:
|
|
372
|
-
session = session_manager.get_session(session_id)
|
|
373
|
-
if session:
|
|
374
|
-
session.detach() # Keep PTY alive for reconnection
|
|
375
|
-
|
|
376
|
-
print(f"[Terminal] Client disconnected: {sid}")
|
|
377
|
-
except Exception as e:
|
|
378
|
-
print(f"[Terminal] Error in disconnect handler: {e}")
|
|
379
|
-
|
|
380
|
-
@socketio.on('input')
|
|
381
|
-
def handle_input(data):
|
|
382
|
-
"""Forward input to PTY."""
|
|
383
|
-
try:
|
|
384
|
-
sid = request.sid
|
|
385
|
-
session_id = socket_to_session.get(sid)
|
|
386
|
-
|
|
387
|
-
if session_id:
|
|
388
|
-
session = session_manager.get_session(session_id)
|
|
389
|
-
if session:
|
|
390
|
-
session.write(data)
|
|
391
|
-
except Exception as e:
|
|
392
|
-
print(f"[Terminal] Error in input handler: {e}")
|
|
393
|
-
|
|
394
|
-
@socketio.on('resize')
|
|
395
|
-
def handle_resize(data):
|
|
396
|
-
"""Handle terminal resize."""
|
|
397
|
-
try:
|
|
398
|
-
sid = request.sid
|
|
399
|
-
session_id = socket_to_session.get(sid)
|
|
400
|
-
|
|
401
|
-
if session_id:
|
|
402
|
-
session = session_manager.get_session(session_id)
|
|
403
|
-
if session:
|
|
404
|
-
rows = data.get('rows', 24)
|
|
405
|
-
cols = data.get('cols', 80)
|
|
406
|
-
session.resize(rows, cols)
|
|
407
|
-
except Exception as e:
|
|
408
|
-
print(f"[Terminal] Error in resize handler: {e}")
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
# =============================================================================
|
|
412
|
-
# FEATURE-021: Console Voice Input - Socket.IO Handlers
|
|
413
|
-
# =============================================================================
|
|
414
|
-
|
|
415
|
-
# Global voice service instance
|
|
416
|
-
voice_service = None
|
|
417
|
-
|
|
418
|
-
# Mapping: socket_sid -> voice_session_id
|
|
419
|
-
socket_to_voice_session = {}
|
|
420
|
-
|
|
421
|
-
def register_voice_handlers():
|
|
422
|
-
"""Register WebSocket event handlers for voice input."""
|
|
423
|
-
from x_ipe.services.voice_input_service_v2 import VoiceInputService, is_voice_command
|
|
424
|
-
|
|
425
|
-
global voice_service
|
|
426
|
-
|
|
427
|
-
# Initialize voice service with API key from environment
|
|
428
|
-
api_key = os.environ.get('ALIBABA_SPEECH_API_KEY', '')
|
|
429
|
-
if api_key:
|
|
430
|
-
voice_service = VoiceInputService(api_key=api_key)
|
|
431
|
-
print("[Voice] Voice service initialized with API key")
|
|
432
|
-
else:
|
|
433
|
-
print("[Voice] No ALIBABA_SPEECH_API_KEY found, voice input disabled")
|
|
434
|
-
|
|
435
|
-
@socketio.on('voice_start')
|
|
436
|
-
def handle_voice_start(data=None):
|
|
437
|
-
"""Handle voice recording start request."""
|
|
438
|
-
global voice_service
|
|
439
|
-
sid = request.sid
|
|
440
|
-
|
|
441
|
-
print(f"[Voice] 🎬 voice_start received from {sid}")
|
|
442
|
-
print(f"[Voice] data: {data}")
|
|
443
|
-
|
|
444
|
-
if not voice_service:
|
|
445
|
-
print(f"[Voice] ❌ Voice service not configured!")
|
|
446
|
-
emit('voice_error', {'message': 'Voice service not configured. Set ALIBABA_SPEECH_API_KEY in x-ipe-docs/config/.env'})
|
|
447
|
-
return
|
|
448
|
-
|
|
449
|
-
try:
|
|
450
|
-
# Create voice session with callbacks for partial results
|
|
451
|
-
def on_partial(text):
|
|
452
|
-
print(f"[Voice] 📤 Emitting voice_partial: '{text}'")
|
|
453
|
-
socketio.emit('voice_partial', {'text': text}, room=sid)
|
|
454
|
-
|
|
455
|
-
session_id = voice_service.create_session(
|
|
456
|
-
socket_sid=sid,
|
|
457
|
-
on_partial=on_partial,
|
|
458
|
-
)
|
|
459
|
-
socket_to_voice_session[sid] = session_id
|
|
460
|
-
|
|
461
|
-
# Start recognition
|
|
462
|
-
print(f"[Voice] Starting recognition...")
|
|
463
|
-
if voice_service.start_recognition(session_id):
|
|
464
|
-
print(f"[Voice] ✅ Emitting voice_ready for session {session_id}")
|
|
465
|
-
emit('voice_ready', {'session_id': session_id})
|
|
466
|
-
else:
|
|
467
|
-
print(f"[Voice] ❌ Failed to start recognition")
|
|
468
|
-
emit('voice_error', {'message': 'Failed to start recognition'})
|
|
469
|
-
voice_service.remove_session(session_id)
|
|
470
|
-
del socket_to_voice_session[sid]
|
|
471
|
-
except Exception as e:
|
|
472
|
-
print(f"[Voice] ❌ Exception in voice_start: {e}")
|
|
473
|
-
import traceback
|
|
474
|
-
traceback.print_exc()
|
|
475
|
-
emit('voice_error', {'message': str(e)})
|
|
476
|
-
|
|
477
|
-
@socketio.on('voice_audio')
|
|
478
|
-
def handle_voice_audio(data):
|
|
479
|
-
"""Handle incoming audio chunk from client."""
|
|
480
|
-
global voice_service
|
|
481
|
-
sid = request.sid
|
|
482
|
-
|
|
483
|
-
if not voice_service:
|
|
484
|
-
return
|
|
485
|
-
|
|
486
|
-
session_id = socket_to_voice_session.get(sid)
|
|
487
|
-
if not session_id:
|
|
488
|
-
print(f"[Voice] ⚠️ voice_audio received but no session for {sid}")
|
|
489
|
-
return
|
|
490
|
-
|
|
491
|
-
try:
|
|
492
|
-
# Get audio data from message
|
|
493
|
-
audio_chunk = data.get('audio', b'') if isinstance(data, dict) else data
|
|
494
|
-
if isinstance(audio_chunk, list):
|
|
495
|
-
audio_chunk = bytes(audio_chunk)
|
|
496
|
-
|
|
497
|
-
# Forward to voice service (sync now with dashscope SDK)
|
|
498
|
-
voice_service.send_audio(session_id, audio_chunk)
|
|
499
|
-
except Exception as e:
|
|
500
|
-
print(f"[Voice] ❌ Exception in voice_audio: {e}")
|
|
501
|
-
emit('voice_error', {'message': f'Audio error: {e}'})
|
|
502
|
-
|
|
503
|
-
@socketio.on('voice_stop')
|
|
504
|
-
def handle_voice_stop(data=None):
|
|
505
|
-
"""Handle voice recording stop request - finalize and get transcription."""
|
|
506
|
-
global voice_service
|
|
507
|
-
sid = request.sid
|
|
508
|
-
|
|
509
|
-
print(f"[Voice] 🛑 voice_stop received from {sid}")
|
|
510
|
-
|
|
511
|
-
if not voice_service:
|
|
512
|
-
print(f"[Voice] ❌ Voice service not available")
|
|
513
|
-
emit('voice_error', {'message': 'Voice service not available'})
|
|
514
|
-
return
|
|
515
|
-
|
|
516
|
-
session_id = socket_to_voice_session.get(sid)
|
|
517
|
-
if not session_id:
|
|
518
|
-
print(f"[Voice] ❌ No active voice session for {sid}")
|
|
519
|
-
emit('voice_error', {'message': 'No active voice session'})
|
|
520
|
-
return
|
|
521
|
-
|
|
522
|
-
try:
|
|
523
|
-
print(f"[Voice] Stopping recognition for session {session_id}...")
|
|
524
|
-
# Stop recognition and get result (sync with dashscope SDK)
|
|
525
|
-
result = voice_service.stop_recognition(session_id)
|
|
526
|
-
|
|
527
|
-
print(f"[Voice] Result: '{result}'")
|
|
528
|
-
|
|
529
|
-
# Check if result is a voice command
|
|
530
|
-
command = is_voice_command(result)
|
|
531
|
-
|
|
532
|
-
if command:
|
|
533
|
-
print(f"[Voice] 📤 Emitting voice_command: {command}")
|
|
534
|
-
emit('voice_command', {'command': command, 'text': result})
|
|
535
|
-
else:
|
|
536
|
-
print(f"[Voice] 📤 Emitting voice_result: '{result}'")
|
|
537
|
-
emit('voice_result', {'text': result})
|
|
538
|
-
|
|
539
|
-
# Cleanup session
|
|
540
|
-
voice_service.remove_session(session_id)
|
|
541
|
-
if sid in socket_to_voice_session:
|
|
542
|
-
del socket_to_voice_session[sid]
|
|
543
|
-
|
|
544
|
-
print(f"[Voice] ✅ Session completed: {session_id}")
|
|
545
|
-
except Exception as e:
|
|
546
|
-
print(f"[Voice] ❌ Exception in voice_stop: {e}")
|
|
547
|
-
import traceback
|
|
548
|
-
traceback.print_exc()
|
|
549
|
-
emit('voice_error', {'message': str(e)})
|
|
550
|
-
|
|
551
|
-
@socketio.on('voice_cancel')
|
|
552
|
-
def handle_voice_cancel(data=None):
|
|
553
|
-
"""Handle voice recording cancel request - abort without transcription."""
|
|
554
|
-
global voice_service
|
|
555
|
-
sid = request.sid
|
|
556
|
-
|
|
557
|
-
print(f"[Voice] ⚠️ voice_cancel received from {sid}")
|
|
558
|
-
|
|
559
|
-
session_id = socket_to_voice_session.get(sid)
|
|
560
|
-
if not session_id:
|
|
561
|
-
print(f"[Voice] No session to cancel")
|
|
562
|
-
return
|
|
563
|
-
|
|
564
|
-
try:
|
|
565
|
-
# Cancel recognition (sync with dashscope SDK)
|
|
566
|
-
if voice_service:
|
|
567
|
-
voice_service.cancel_recognition(session_id)
|
|
568
|
-
voice_service.remove_session(session_id)
|
|
569
|
-
|
|
570
|
-
if sid in socket_to_voice_session:
|
|
571
|
-
del socket_to_voice_session[sid]
|
|
572
|
-
|
|
573
|
-
emit('voice_cancelled', {})
|
|
574
|
-
print(f"[Voice] ✅ Session cancelled: {session_id}")
|
|
575
|
-
except Exception as e:
|
|
576
|
-
print(f"[Voice] ❌ Exception in voice_cancel: {e}")
|
|
577
|
-
emit('voice_error', {'message': f'Cancel error: {e}'})
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
# =============================================================================
|
|
581
|
-
# FEATURE-006: Settings & Configuration - Routes
|
|
582
|
-
# =============================================================================
|
|
583
|
-
|
|
584
|
-
def register_settings_routes(app):
|
|
585
|
-
"""Register settings API and page routes."""
|
|
586
|
-
|
|
587
|
-
@app.route('/settings')
|
|
588
|
-
def settings_page():
|
|
589
|
-
"""
|
|
590
|
-
GET /settings
|
|
591
|
-
|
|
592
|
-
Render the settings page.
|
|
593
|
-
"""
|
|
594
|
-
global settings_service
|
|
595
|
-
current_settings = settings_service.get_all() if settings_service else {'project_root': '.'}
|
|
596
|
-
return render_template('settings.html', settings=current_settings)
|
|
597
|
-
|
|
598
|
-
@app.route('/api/settings', methods=['GET'])
|
|
599
|
-
def get_settings():
|
|
600
|
-
"""
|
|
601
|
-
GET /api/settings
|
|
602
|
-
|
|
603
|
-
Get all current settings.
|
|
604
|
-
|
|
605
|
-
Response:
|
|
606
|
-
- project_root: string - Current project root path
|
|
607
|
-
"""
|
|
608
|
-
global settings_service
|
|
609
|
-
if not settings_service:
|
|
610
|
-
return jsonify({'project_root': app.config.get('PROJECT_ROOT', '.')}), 200
|
|
611
|
-
|
|
612
|
-
return jsonify(settings_service.get_all())
|
|
613
|
-
|
|
614
|
-
@app.route('/api/settings', methods=['POST'])
|
|
615
|
-
def save_settings():
|
|
616
|
-
"""
|
|
617
|
-
POST /api/settings
|
|
618
|
-
|
|
619
|
-
Save settings.
|
|
620
|
-
|
|
621
|
-
Request body:
|
|
622
|
-
- project_root: string (optional) - New project root path
|
|
623
|
-
|
|
624
|
-
Response (success):
|
|
625
|
-
- success: true
|
|
626
|
-
- message: string
|
|
627
|
-
|
|
628
|
-
Response (error):
|
|
629
|
-
- success: false
|
|
630
|
-
- errors: object with field-specific error messages
|
|
631
|
-
"""
|
|
632
|
-
global settings_service, file_watcher
|
|
633
|
-
|
|
634
|
-
data = request.get_json() or {}
|
|
635
|
-
errors = {}
|
|
636
|
-
|
|
637
|
-
# Validate project_root if provided
|
|
638
|
-
if 'project_root' in data:
|
|
639
|
-
path = data['project_root']
|
|
640
|
-
path_errors = settings_service.validate_project_root(path)
|
|
641
|
-
errors.update(path_errors)
|
|
642
|
-
|
|
643
|
-
# Return errors if any
|
|
644
|
-
if errors:
|
|
645
|
-
return jsonify({'success': False, 'errors': errors}), 400
|
|
646
|
-
|
|
647
|
-
# Save settings
|
|
648
|
-
for key, value in data.items():
|
|
649
|
-
if key in ['project_root']: # Allowed settings
|
|
650
|
-
settings_service.set(key, value)
|
|
651
|
-
|
|
652
|
-
# Apply project_root change
|
|
653
|
-
if 'project_root' in data:
|
|
654
|
-
new_path = data['project_root']
|
|
655
|
-
app.config['PROJECT_ROOT'] = new_path
|
|
656
|
-
|
|
657
|
-
return jsonify({'success': True, 'message': 'Settings saved successfully'})
|
|
658
|
-
|
|
659
|
-
@app.route('/api/config', methods=['GET'])
|
|
660
|
-
def get_config():
|
|
661
|
-
"""
|
|
662
|
-
GET /api/config
|
|
663
|
-
|
|
664
|
-
Get current project configuration from .x-ipe.yaml.
|
|
665
|
-
|
|
666
|
-
FEATURE-010: Project Root Configuration
|
|
667
|
-
|
|
668
|
-
Response (config detected):
|
|
669
|
-
- detected: true
|
|
670
|
-
- config_file: string - Path to .x-ipe.yaml
|
|
671
|
-
- version: int
|
|
672
|
-
- project_root: string
|
|
673
|
-
- x_ipe_app: string
|
|
674
|
-
- file_tree_scope: string
|
|
675
|
-
- terminal_cwd: string
|
|
676
|
-
|
|
677
|
-
Response (no config):
|
|
678
|
-
- detected: false
|
|
679
|
-
- config_file: null
|
|
680
|
-
- using_defaults: true
|
|
681
|
-
- project_root: string - Current project root
|
|
682
|
-
- message: string
|
|
683
|
-
"""
|
|
684
|
-
config_data = app.config.get('X_IPE_CONFIG')
|
|
685
|
-
|
|
686
|
-
if config_data:
|
|
687
|
-
return jsonify({
|
|
688
|
-
'detected': True,
|
|
689
|
-
**config_data.to_dict()
|
|
690
|
-
})
|
|
691
|
-
else:
|
|
692
|
-
return jsonify({
|
|
693
|
-
'detected': False,
|
|
694
|
-
'config_file': None,
|
|
695
|
-
'using_defaults': True,
|
|
696
|
-
'project_root': app.config.get('PROJECT_ROOT', os.getcwd()),
|
|
697
|
-
'message': 'No .x-ipe.yaml found. Using default paths.'
|
|
698
|
-
})
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
def register_project_routes(app):
|
|
702
|
-
"""
|
|
703
|
-
Register project folder management routes.
|
|
704
|
-
|
|
705
|
-
FEATURE-006 v2.0: Multi-Project Folder Support
|
|
706
|
-
"""
|
|
707
|
-
|
|
708
|
-
@app.route('/api/projects', methods=['GET'])
|
|
709
|
-
def get_projects():
|
|
710
|
-
"""
|
|
711
|
-
GET /api/projects
|
|
712
|
-
|
|
713
|
-
Get all project folders and active project ID.
|
|
714
|
-
|
|
715
|
-
Response:
|
|
716
|
-
- projects: array of {id, name, path}
|
|
717
|
-
- active_project_id: number
|
|
718
|
-
"""
|
|
719
|
-
global project_folders_service
|
|
720
|
-
if not project_folders_service:
|
|
721
|
-
return jsonify({'projects': [], 'active_project_id': 1}), 200
|
|
722
|
-
|
|
723
|
-
return jsonify({
|
|
724
|
-
'projects': project_folders_service.get_all(),
|
|
725
|
-
'active_project_id': project_folders_service.get_active_id()
|
|
726
|
-
})
|
|
727
|
-
|
|
728
|
-
@app.route('/api/projects', methods=['POST'])
|
|
729
|
-
def add_project():
|
|
730
|
-
"""
|
|
731
|
-
POST /api/projects
|
|
732
|
-
|
|
733
|
-
Add a new project folder.
|
|
734
|
-
|
|
735
|
-
Request body:
|
|
736
|
-
- name: string - Project name
|
|
737
|
-
- path: string - Project path
|
|
738
|
-
|
|
739
|
-
Response (success):
|
|
740
|
-
- success: true
|
|
741
|
-
- project: {id, name, path}
|
|
742
|
-
|
|
743
|
-
Response (error):
|
|
744
|
-
- success: false
|
|
745
|
-
- errors: object with field-specific error messages
|
|
746
|
-
"""
|
|
747
|
-
global project_folders_service
|
|
748
|
-
|
|
749
|
-
if not request.is_json:
|
|
750
|
-
return jsonify({'success': False, 'error': 'JSON required'}), 400
|
|
751
|
-
|
|
752
|
-
data = request.get_json()
|
|
753
|
-
name = data.get('name', '').strip()
|
|
754
|
-
path = data.get('path', '').strip()
|
|
755
|
-
|
|
756
|
-
result = project_folders_service.add(name, path)
|
|
757
|
-
|
|
758
|
-
if result['success']:
|
|
759
|
-
return jsonify(result), 201
|
|
760
|
-
return jsonify(result), 400
|
|
761
|
-
|
|
762
|
-
@app.route('/api/projects/<int:project_id>', methods=['PUT'])
|
|
763
|
-
def update_project(project_id):
|
|
764
|
-
"""
|
|
765
|
-
PUT /api/projects/<id>
|
|
766
|
-
|
|
767
|
-
Update an existing project folder.
|
|
768
|
-
|
|
769
|
-
Request body:
|
|
770
|
-
- name: string (optional) - New project name
|
|
771
|
-
- path: string (optional) - New project path
|
|
772
|
-
|
|
773
|
-
Response (success):
|
|
774
|
-
- success: true
|
|
775
|
-
- project: {id, name, path}
|
|
776
|
-
|
|
777
|
-
Response (error):
|
|
778
|
-
- success: false
|
|
779
|
-
- errors: object with field-specific error messages
|
|
780
|
-
"""
|
|
781
|
-
global project_folders_service
|
|
782
|
-
|
|
783
|
-
if not request.is_json:
|
|
784
|
-
return jsonify({'success': False, 'error': 'JSON required'}), 400
|
|
785
|
-
|
|
786
|
-
data = request.get_json()
|
|
787
|
-
name = data.get('name')
|
|
788
|
-
path = data.get('path')
|
|
789
|
-
|
|
790
|
-
result = project_folders_service.update(project_id, name=name, path=path)
|
|
791
|
-
|
|
792
|
-
if result['success']:
|
|
793
|
-
return jsonify(result)
|
|
794
|
-
return jsonify(result), 400
|
|
795
|
-
|
|
796
|
-
@app.route('/api/projects/<int:project_id>', methods=['DELETE'])
|
|
797
|
-
def delete_project(project_id):
|
|
798
|
-
"""
|
|
799
|
-
DELETE /api/projects/<id>
|
|
800
|
-
|
|
801
|
-
Delete a project folder.
|
|
802
|
-
|
|
803
|
-
Response (success):
|
|
804
|
-
- success: true
|
|
805
|
-
|
|
806
|
-
Response (error):
|
|
807
|
-
- success: false
|
|
808
|
-
- error: string error message
|
|
809
|
-
"""
|
|
810
|
-
global project_folders_service
|
|
811
|
-
|
|
812
|
-
active_id = project_folders_service.get_active_id()
|
|
813
|
-
result = project_folders_service.delete(project_id, active_project_id=active_id)
|
|
814
|
-
|
|
815
|
-
if result['success']:
|
|
816
|
-
return jsonify(result)
|
|
817
|
-
return jsonify(result), 400
|
|
818
|
-
|
|
819
|
-
@app.route('/api/projects/switch', methods=['POST'])
|
|
820
|
-
def switch_project():
|
|
821
|
-
"""
|
|
822
|
-
POST /api/projects/switch
|
|
823
|
-
|
|
824
|
-
Switch the active project.
|
|
825
|
-
|
|
826
|
-
Request body:
|
|
827
|
-
- project_id: number - Project ID to switch to
|
|
828
|
-
|
|
829
|
-
Response (success):
|
|
830
|
-
- success: true
|
|
831
|
-
- active_project_id: number
|
|
832
|
-
- project: {id, name, path}
|
|
833
|
-
|
|
834
|
-
Response (error):
|
|
835
|
-
- success: false
|
|
836
|
-
- error: string error message
|
|
837
|
-
"""
|
|
838
|
-
global project_folders_service
|
|
839
|
-
|
|
840
|
-
if not request.is_json:
|
|
841
|
-
return jsonify({'success': False, 'error': 'JSON required'}), 400
|
|
842
|
-
|
|
843
|
-
data = request.get_json()
|
|
844
|
-
project_id = data.get('project_id')
|
|
845
|
-
|
|
846
|
-
if not project_id:
|
|
847
|
-
return jsonify({'success': False, 'error': 'project_id required'}), 400
|
|
848
|
-
|
|
849
|
-
result = project_folders_service.set_active(project_id)
|
|
850
|
-
|
|
851
|
-
if result['success']:
|
|
852
|
-
# Update app config with new project root
|
|
853
|
-
project = result['project']
|
|
854
|
-
project_path = project['path']
|
|
855
|
-
|
|
856
|
-
# If config is detected, project folders are relative to config's project_root
|
|
857
|
-
config_data = app.config.get('X_IPE_CONFIG')
|
|
858
|
-
if config_data:
|
|
859
|
-
# Config always takes precedence - use its project_root
|
|
860
|
-
app.config['PROJECT_ROOT'] = config_data.project_root
|
|
861
|
-
elif project_path == '.':
|
|
862
|
-
# No config, default project folder - use cwd where x-ipe was run
|
|
863
|
-
app.config['PROJECT_ROOT'] = os.environ.get('X_IPE_PROJECT_ROOT', os.getcwd())
|
|
864
|
-
else:
|
|
865
|
-
# Absolute path from project folder
|
|
866
|
-
app.config['PROJECT_ROOT'] = project_path
|
|
867
|
-
|
|
868
|
-
return jsonify(result)
|
|
869
|
-
return jsonify(result), 400
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
def register_ideas_routes(app):
|
|
873
|
-
"""
|
|
874
|
-
Register idea management routes.
|
|
875
|
-
|
|
876
|
-
FEATURE-008: Workplace (Idea Management)
|
|
877
|
-
"""
|
|
878
|
-
|
|
879
|
-
@app.route('/api/ideas/tree', methods=['GET'])
|
|
880
|
-
def get_ideas_tree():
|
|
881
|
-
"""
|
|
882
|
-
GET /api/ideas/tree
|
|
883
|
-
|
|
884
|
-
Get tree structure of x-ipe-docs/ideas/ directory.
|
|
885
|
-
|
|
886
|
-
Response:
|
|
887
|
-
- success: true
|
|
888
|
-
- tree: array of folder/file objects
|
|
889
|
-
"""
|
|
890
|
-
project_root = app.config.get('PROJECT_ROOT', os.getcwd())
|
|
891
|
-
service = IdeasService(project_root)
|
|
892
|
-
|
|
893
|
-
try:
|
|
894
|
-
tree = service.get_tree()
|
|
895
|
-
return jsonify({
|
|
896
|
-
'success': True,
|
|
897
|
-
'tree': tree
|
|
898
|
-
})
|
|
899
|
-
except Exception as e:
|
|
900
|
-
return jsonify({
|
|
901
|
-
'success': False,
|
|
902
|
-
'error': str(e)
|
|
903
|
-
}), 500
|
|
904
|
-
|
|
905
|
-
@app.route('/api/ideas/upload', methods=['POST'])
|
|
906
|
-
def upload_ideas():
|
|
907
|
-
"""
|
|
908
|
-
POST /api/ideas/upload
|
|
909
|
-
|
|
910
|
-
Upload files to a new or existing idea folder.
|
|
911
|
-
|
|
912
|
-
Request: multipart/form-data with 'files' field
|
|
913
|
-
Optional: 'target_folder' field to upload to existing folder (CR-002)
|
|
914
|
-
|
|
915
|
-
Response:
|
|
916
|
-
- success: true/false
|
|
917
|
-
- folder_name: string
|
|
918
|
-
- folder_path: string
|
|
919
|
-
- files_uploaded: array of filenames
|
|
920
|
-
"""
|
|
921
|
-
project_root = app.config.get('PROJECT_ROOT', os.getcwd())
|
|
922
|
-
service = IdeasService(project_root)
|
|
923
|
-
|
|
924
|
-
if 'files' not in request.files:
|
|
925
|
-
return jsonify({
|
|
926
|
-
'success': False,
|
|
927
|
-
'error': 'No files provided'
|
|
928
|
-
}), 400
|
|
929
|
-
|
|
930
|
-
uploaded_files = request.files.getlist('files')
|
|
931
|
-
if not uploaded_files or all(f.filename == '' for f in uploaded_files):
|
|
932
|
-
return jsonify({
|
|
933
|
-
'success': False,
|
|
934
|
-
'error': 'No files provided'
|
|
935
|
-
}), 400
|
|
936
|
-
|
|
937
|
-
# CR-002: Get optional target_folder from form data
|
|
938
|
-
target_folder = request.form.get('target_folder', None)
|
|
939
|
-
|
|
940
|
-
# Convert to (filename, content) tuples
|
|
941
|
-
files = [(f.filename, f.read()) for f in uploaded_files if f.filename]
|
|
942
|
-
|
|
943
|
-
result = service.upload(files, target_folder=target_folder)
|
|
944
|
-
|
|
945
|
-
if result['success']:
|
|
946
|
-
return jsonify(result)
|
|
947
|
-
return jsonify(result), 400
|
|
948
|
-
|
|
949
|
-
@app.route('/api/ideas/rename', methods=['POST'])
|
|
950
|
-
def rename_idea_folder():
|
|
951
|
-
"""
|
|
952
|
-
POST /api/ideas/rename
|
|
953
|
-
|
|
954
|
-
Rename an idea folder.
|
|
955
|
-
|
|
956
|
-
Request body:
|
|
957
|
-
- old_name: string - Current folder name
|
|
958
|
-
- new_name: string - New folder name
|
|
959
|
-
|
|
960
|
-
Response:
|
|
961
|
-
- success: true/false
|
|
962
|
-
- old_name: string
|
|
963
|
-
- new_name: string
|
|
964
|
-
- new_path: string
|
|
965
|
-
"""
|
|
966
|
-
project_root = app.config.get('PROJECT_ROOT', os.getcwd())
|
|
967
|
-
service = IdeasService(project_root)
|
|
968
|
-
|
|
969
|
-
if not request.is_json:
|
|
970
|
-
return jsonify({
|
|
971
|
-
'success': False,
|
|
972
|
-
'error': 'JSON required'
|
|
973
|
-
}), 400
|
|
974
|
-
|
|
975
|
-
data = request.get_json()
|
|
976
|
-
old_name = data.get('old_name')
|
|
977
|
-
new_name = data.get('new_name')
|
|
978
|
-
|
|
979
|
-
if not old_name or not new_name:
|
|
980
|
-
return jsonify({
|
|
981
|
-
'success': False,
|
|
982
|
-
'error': 'old_name and new_name are required'
|
|
983
|
-
}), 400
|
|
984
|
-
|
|
985
|
-
result = service.rename_folder(old_name, new_name)
|
|
986
|
-
|
|
987
|
-
if result['success']:
|
|
988
|
-
return jsonify(result)
|
|
989
|
-
return jsonify(result), 400
|
|
990
|
-
|
|
991
|
-
@app.route('/api/ideas/rename-file', methods=['POST'])
|
|
992
|
-
def rename_idea_file():
|
|
993
|
-
"""
|
|
994
|
-
POST /api/ideas/rename-file
|
|
995
|
-
|
|
996
|
-
Rename a file within x-ipe-docs/ideas/.
|
|
997
|
-
|
|
998
|
-
Request body:
|
|
999
|
-
- path: string - Current file path (relative to project root)
|
|
1000
|
-
- new_name: string - New file name (with extension)
|
|
1001
|
-
|
|
1002
|
-
Response:
|
|
1003
|
-
- success: true/false
|
|
1004
|
-
- old_path: string
|
|
1005
|
-
- new_path: string
|
|
1006
|
-
- new_name: string
|
|
1007
|
-
"""
|
|
1008
|
-
project_root = app.config.get('PROJECT_ROOT', os.getcwd())
|
|
1009
|
-
service = IdeasService(project_root)
|
|
1010
|
-
|
|
1011
|
-
if not request.is_json:
|
|
1012
|
-
return jsonify({
|
|
1013
|
-
'success': False,
|
|
1014
|
-
'error': 'JSON required'
|
|
1015
|
-
}), 400
|
|
1016
|
-
|
|
1017
|
-
data = request.get_json()
|
|
1018
|
-
path = data.get('path')
|
|
1019
|
-
new_name = data.get('new_name')
|
|
1020
|
-
|
|
1021
|
-
if not path or not new_name:
|
|
1022
|
-
return jsonify({
|
|
1023
|
-
'success': False,
|
|
1024
|
-
'error': 'path and new_name are required'
|
|
1025
|
-
}), 400
|
|
1026
|
-
|
|
1027
|
-
result = service.rename_file(path, new_name)
|
|
1028
|
-
|
|
1029
|
-
if result['success']:
|
|
1030
|
-
return jsonify(result)
|
|
1031
|
-
return jsonify(result), 400
|
|
1032
|
-
|
|
1033
|
-
@app.route('/api/ideas/delete', methods=['POST'])
|
|
1034
|
-
def delete_idea_item():
|
|
1035
|
-
"""
|
|
1036
|
-
POST /api/ideas/delete
|
|
1037
|
-
|
|
1038
|
-
Delete an idea file or folder.
|
|
1039
|
-
|
|
1040
|
-
Request body:
|
|
1041
|
-
- path: string - Relative path to file/folder within x-ipe-docs/ideas/
|
|
1042
|
-
|
|
1043
|
-
Response:
|
|
1044
|
-
- success: true/false
|
|
1045
|
-
- path: string
|
|
1046
|
-
- type: 'file' | 'folder'
|
|
1047
|
-
"""
|
|
1048
|
-
project_root = app.config.get('PROJECT_ROOT', os.getcwd())
|
|
1049
|
-
service = IdeasService(project_root)
|
|
1050
|
-
|
|
1051
|
-
if not request.is_json:
|
|
1052
|
-
return jsonify({
|
|
1053
|
-
'success': False,
|
|
1054
|
-
'error': 'JSON required'
|
|
1055
|
-
}), 400
|
|
1056
|
-
|
|
1057
|
-
data = request.get_json()
|
|
1058
|
-
path = data.get('path')
|
|
1059
|
-
|
|
1060
|
-
if not path:
|
|
1061
|
-
return jsonify({
|
|
1062
|
-
'success': False,
|
|
1063
|
-
'error': 'path is required'
|
|
1064
|
-
}), 400
|
|
1065
|
-
|
|
1066
|
-
result = service.delete_item(path)
|
|
1067
|
-
|
|
1068
|
-
if result['success']:
|
|
1069
|
-
return jsonify(result)
|
|
1070
|
-
return jsonify(result), 400
|
|
1071
|
-
|
|
1072
|
-
@app.route('/api/ideas/toolbox', methods=['GET'])
|
|
1073
|
-
def get_ideas_toolbox():
|
|
1074
|
-
"""
|
|
1075
|
-
GET /api/ideas/toolbox
|
|
1076
|
-
|
|
1077
|
-
Get ideation toolbox configuration.
|
|
1078
|
-
|
|
1079
|
-
Response:
|
|
1080
|
-
- version: string
|
|
1081
|
-
- ideation: {antv-infographic: bool, mermaid: bool}
|
|
1082
|
-
- mockup: {frontend-design: bool}
|
|
1083
|
-
- sharing: {}
|
|
1084
|
-
"""
|
|
1085
|
-
project_root = app.config.get('PROJECT_ROOT', os.getcwd())
|
|
1086
|
-
service = IdeasService(project_root)
|
|
1087
|
-
|
|
1088
|
-
config = service.get_toolbox()
|
|
1089
|
-
return jsonify(config)
|
|
1090
|
-
|
|
1091
|
-
@app.route('/api/ideas/toolbox', methods=['POST'])
|
|
1092
|
-
def save_ideas_toolbox():
|
|
1093
|
-
"""
|
|
1094
|
-
POST /api/ideas/toolbox
|
|
1095
|
-
|
|
1096
|
-
Save ideation toolbox configuration.
|
|
1097
|
-
|
|
1098
|
-
Request body:
|
|
1099
|
-
- version: string
|
|
1100
|
-
- ideation: {antv-infographic: bool, mermaid: bool}
|
|
1101
|
-
- mockup: {frontend-design: bool}
|
|
1102
|
-
- sharing: {}
|
|
1103
|
-
|
|
1104
|
-
Response:
|
|
1105
|
-
- success: true/false
|
|
1106
|
-
"""
|
|
1107
|
-
project_root = app.config.get('PROJECT_ROOT', os.getcwd())
|
|
1108
|
-
service = IdeasService(project_root)
|
|
1109
|
-
|
|
1110
|
-
config = request.get_json()
|
|
1111
|
-
result = service.save_toolbox(config)
|
|
1112
|
-
|
|
1113
|
-
if result['success']:
|
|
1114
|
-
return jsonify(result)
|
|
1115
|
-
return jsonify(result), 400
|
|
1116
|
-
|
|
1117
|
-
"""
|
|
1118
|
-
==========================================================================
|
|
1119
|
-
SKILLS API
|
|
1120
|
-
|
|
1121
|
-
Read-only API for skills defined in .github/skills/
|
|
1122
|
-
==========================================================================
|
|
1123
|
-
"""
|
|
1124
|
-
|
|
1125
|
-
@app.route('/api/skills', methods=['GET'])
|
|
1126
|
-
def get_skills():
|
|
1127
|
-
"""
|
|
1128
|
-
GET /api/skills
|
|
1129
|
-
|
|
1130
|
-
Get list of all skills with name and description.
|
|
1131
|
-
|
|
1132
|
-
Response:
|
|
1133
|
-
- success: true
|
|
1134
|
-
- skills: array of {name, description}
|
|
1135
|
-
"""
|
|
1136
|
-
project_root = app.config.get('PROJECT_ROOT', os.getcwd())
|
|
1137
|
-
service = SkillsService(project_root)
|
|
1138
|
-
|
|
1139
|
-
try:
|
|
1140
|
-
skills = service.get_all()
|
|
1141
|
-
return jsonify({
|
|
1142
|
-
'success': True,
|
|
1143
|
-
'skills': skills
|
|
1144
|
-
})
|
|
1145
|
-
except Exception as e:
|
|
1146
|
-
return jsonify({
|
|
1147
|
-
'success': False,
|
|
1148
|
-
'error': str(e)
|
|
1149
|
-
}), 500
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
# Entry point for running with `python -m src.app`
|
|
1153
|
-
def register_tools_config_routes(app):
|
|
1154
|
-
"""
|
|
1155
|
-
Register tools configuration routes.
|
|
1156
|
-
|
|
1157
|
-
FEATURE-011: Stage Toolbox
|
|
1158
|
-
"""
|
|
1159
|
-
|
|
1160
|
-
def _get_tools_service():
|
|
1161
|
-
"""Get ToolsConfigService instance for current project root."""
|
|
1162
|
-
project_root = app.config.get('PROJECT_ROOT', os.getcwd())
|
|
1163
|
-
return ToolsConfigService(project_root)
|
|
1164
|
-
|
|
1165
|
-
@app.route('/api/config/tools', methods=['GET'])
|
|
1166
|
-
def get_tools_config():
|
|
1167
|
-
"""
|
|
1168
|
-
GET /api/config/tools
|
|
1169
|
-
|
|
1170
|
-
Get current tools configuration.
|
|
1171
|
-
|
|
1172
|
-
Response:
|
|
1173
|
-
- success: true
|
|
1174
|
-
- config: tools configuration object
|
|
1175
|
-
"""
|
|
1176
|
-
try:
|
|
1177
|
-
service = _get_tools_service()
|
|
1178
|
-
config = service.load()
|
|
1179
|
-
return jsonify({
|
|
1180
|
-
'success': True,
|
|
1181
|
-
'config': config
|
|
1182
|
-
})
|
|
1183
|
-
except Exception as e:
|
|
1184
|
-
return jsonify({
|
|
1185
|
-
'success': False,
|
|
1186
|
-
'error': str(e)
|
|
1187
|
-
}), 500
|
|
1188
|
-
|
|
1189
|
-
@app.route('/api/config/tools', methods=['POST'])
|
|
1190
|
-
def save_tools_config():
|
|
1191
|
-
"""
|
|
1192
|
-
POST /api/config/tools
|
|
1193
|
-
|
|
1194
|
-
Save tools configuration.
|
|
1195
|
-
|
|
1196
|
-
Request Body: JSON with 'stages' key
|
|
1197
|
-
|
|
1198
|
-
Response:
|
|
1199
|
-
- success: true/false
|
|
1200
|
-
- error: string (on failure)
|
|
1201
|
-
"""
|
|
1202
|
-
try:
|
|
1203
|
-
config = request.get_json(force=True, silent=True)
|
|
1204
|
-
if config is None:
|
|
1205
|
-
return jsonify({
|
|
1206
|
-
'success': False,
|
|
1207
|
-
'error': 'Invalid JSON or empty body'
|
|
1208
|
-
}), 400
|
|
1209
|
-
|
|
1210
|
-
if 'stages' not in config:
|
|
1211
|
-
return jsonify({
|
|
1212
|
-
'success': False,
|
|
1213
|
-
'error': 'Invalid config format: missing stages key'
|
|
1214
|
-
}), 400
|
|
1215
|
-
|
|
1216
|
-
service = _get_tools_service()
|
|
1217
|
-
service.save(config)
|
|
1218
|
-
return jsonify({'success': True})
|
|
1219
|
-
except Exception as e:
|
|
1220
|
-
return jsonify({
|
|
1221
|
-
'success': False,
|
|
1222
|
-
'error': str(e)
|
|
1223
|
-
}), 500
|
|
1224
|
-
|
|
1225
|
-
@app.route('/api/config/copilot-prompt', methods=['GET'])
|
|
1226
|
-
def get_copilot_prompt_config():
|
|
1227
|
-
"""
|
|
1228
|
-
GET /api/config/copilot-prompt
|
|
1229
|
-
|
|
1230
|
-
Get Copilot prompt configuration for the Copilot button dropdown.
|
|
1231
|
-
|
|
1232
|
-
Response:
|
|
1233
|
-
- prompts: List of prompt objects with id, label, icon, command
|
|
1234
|
-
- placeholder: Dictionary of placeholder descriptions
|
|
1235
|
-
"""
|
|
1236
|
-
try:
|
|
1237
|
-
project_root = app.config.get('PROJECT_ROOT', os.getcwd())
|
|
1238
|
-
config_path = os.path.join(project_root, 'x-ipe-docs', 'config', 'copilot-prompt.json')
|
|
1239
|
-
|
|
1240
|
-
if os.path.exists(config_path):
|
|
1241
|
-
with open(config_path, 'r', encoding='utf-8') as f:
|
|
1242
|
-
config = json.load(f)
|
|
1243
|
-
return jsonify(config)
|
|
1244
|
-
else:
|
|
1245
|
-
# Return empty prompts if config doesn't exist
|
|
1246
|
-
return jsonify({
|
|
1247
|
-
'version': '1.0',
|
|
1248
|
-
'prompts': [],
|
|
1249
|
-
'placeholder': {}
|
|
1250
|
-
})
|
|
1251
|
-
except Exception as e:
|
|
1252
|
-
return jsonify({
|
|
1253
|
-
'prompts': [],
|
|
1254
|
-
'error': str(e)
|
|
1255
|
-
}), 500
|
|
1256
|
-
|
|
1257
|
-
# ============================================================
|
|
1258
|
-
# FEATURE-012: Design Themes API
|
|
1259
|
-
# ============================================================
|
|
1260
|
-
|
|
1261
|
-
def _get_themes_service():
|
|
1262
|
-
"""Get ThemesService instance for current project root."""
|
|
1263
|
-
project_root = app.config.get('PROJECT_ROOT', os.getcwd())
|
|
1264
|
-
return ThemesService(project_root)
|
|
1265
|
-
|
|
1266
|
-
@app.route('/api/themes', methods=['GET'])
|
|
1267
|
-
def list_themes():
|
|
1268
|
-
"""
|
|
1269
|
-
GET /api/themes
|
|
1270
|
-
|
|
1271
|
-
List all themes with metadata.
|
|
1272
|
-
|
|
1273
|
-
Response:
|
|
1274
|
-
- themes: List of theme objects with name, description, colors, files, path
|
|
1275
|
-
- selected: Currently selected theme name from config (null if none)
|
|
1276
|
-
"""
|
|
1277
|
-
try:
|
|
1278
|
-
themes_service = _get_themes_service()
|
|
1279
|
-
themes = themes_service.list_themes()
|
|
1280
|
-
|
|
1281
|
-
# Get selected theme from tools config (new format)
|
|
1282
|
-
tools_service = _get_tools_service()
|
|
1283
|
-
config = tools_service.load()
|
|
1284
|
-
selected_theme_config = config.get('selected-theme', {})
|
|
1285
|
-
selected = selected_theme_config.get('theme-name') if selected_theme_config else None
|
|
1286
|
-
|
|
1287
|
-
return jsonify({
|
|
1288
|
-
'themes': themes,
|
|
1289
|
-
'selected': selected
|
|
1290
|
-
})
|
|
1291
|
-
except Exception as e:
|
|
1292
|
-
return jsonify({
|
|
1293
|
-
'themes': [],
|
|
1294
|
-
'selected': 'theme-default',
|
|
1295
|
-
'error': str(e)
|
|
1296
|
-
}), 500
|
|
1297
|
-
|
|
1298
|
-
@app.route('/api/themes/<name>', methods=['GET'])
|
|
1299
|
-
def get_theme_detail(name):
|
|
1300
|
-
"""
|
|
1301
|
-
GET /api/themes/<name>
|
|
1302
|
-
|
|
1303
|
-
Get detailed information about a specific theme.
|
|
1304
|
-
|
|
1305
|
-
Args:
|
|
1306
|
-
name: Theme folder name (e.g., "theme-default")
|
|
1307
|
-
|
|
1308
|
-
Response:
|
|
1309
|
-
- Theme detail object with design_system content
|
|
1310
|
-
- 404 if theme not found
|
|
1311
|
-
"""
|
|
1312
|
-
try:
|
|
1313
|
-
themes_service = _get_themes_service()
|
|
1314
|
-
theme = themes_service.get_theme(name)
|
|
1315
|
-
|
|
1316
|
-
if theme is None:
|
|
1317
|
-
return jsonify({
|
|
1318
|
-
'error': f'Theme not found: {name}'
|
|
1319
|
-
}), 404
|
|
1320
|
-
|
|
1321
|
-
return jsonify(theme)
|
|
1322
|
-
except Exception as e:
|
|
1323
|
-
return jsonify({
|
|
1324
|
-
'error': str(e)
|
|
1325
|
-
}), 500
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
if __name__ == '__main__':
|
|
1329
|
-
app = create_app()
|
|
1330
|
-
# Start session cleanup task
|
|
1331
|
-
session_manager.start_cleanup_task()
|
|
1332
|
-
# Use socketio.run for WebSocket support
|
|
1333
|
-
socketio.run(app, debug=True, host='0.0.0.0', port=5858)
|