mindroot 9.5.0__py3-none-any.whl → 9.7.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.
- mindroot/coreplugins/admin/plugin_manager.py +42 -9
- mindroot/coreplugins/admin/static/js/registry-shared-services.js +62 -16
- mindroot/coreplugins/agent/SysAdmin/agent.json +33 -0
- mindroot/coreplugins/agent/init_models.py +3 -0
- mindroot/coreplugins/index/indices/default/index.json +33 -0
- {mindroot-9.5.0.dist-info → mindroot-9.7.0.dist-info}/METADATA +1 -1
- {mindroot-9.5.0.dist-info → mindroot-9.7.0.dist-info}/RECORD +11 -12
- mindroot/coreplugins/admin/plugin_manager_backup.py +0 -615
- mindroot/coreplugins/admin/static/js/registry-manager-old.js +0 -385
- {mindroot-9.5.0.dist-info → mindroot-9.7.0.dist-info}/WHEEL +0 -0
- {mindroot-9.5.0.dist-info → mindroot-9.7.0.dist-info}/entry_points.txt +0 -0
- {mindroot-9.5.0.dist-info → mindroot-9.7.0.dist-info}/licenses/LICENSE +0 -0
- {mindroot-9.5.0.dist-info → mindroot-9.7.0.dist-info}/top_level.txt +0 -0
|
@@ -1,615 +0,0 @@
|
|
|
1
|
-
from fastapi import APIRouter, HTTPException, Request, BackgroundTasks
|
|
2
|
-
from pydantic import BaseModel
|
|
3
|
-
from sse_starlette.sse import EventSourceResponse
|
|
4
|
-
import traceback
|
|
5
|
-
import os
|
|
6
|
-
import json
|
|
7
|
-
from typing import List, Optional
|
|
8
|
-
from lib.plugins import (
|
|
9
|
-
load_plugin_manifest, update_plugin_manifest, plugin_install,
|
|
10
|
-
save_plugin_manifest, plugin_update, toggle_plugin_state, get_plugin_path
|
|
11
|
-
)
|
|
12
|
-
import asyncio
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
router = APIRouter()
|
|
16
|
-
|
|
17
|
-
class DirectoryRequest(BaseModel):
|
|
18
|
-
directory: str
|
|
19
|
-
|
|
20
|
-
class PluginRequest(BaseModel):
|
|
21
|
-
plugin: str
|
|
22
|
-
|
|
23
|
-
class GitHubPluginRequest(BaseModel):
|
|
24
|
-
plugin: str
|
|
25
|
-
url: Optional[str] = None
|
|
26
|
-
github_url: Optional[str] = None
|
|
27
|
-
|
|
28
|
-
class TogglePluginRequest(BaseModel):
|
|
29
|
-
plugin: str
|
|
30
|
-
enabled: bool
|
|
31
|
-
|
|
32
|
-
class InstallFromIndexRequest(BaseModel):
|
|
33
|
-
plugin: str
|
|
34
|
-
index_name: str
|
|
35
|
-
|
|
36
|
-
class StreamInstallRequest(BaseModel):
|
|
37
|
-
plugin: str
|
|
38
|
-
source: str
|
|
39
|
-
source_path: str = None
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
class PluginMetadata(BaseModel):
|
|
43
|
-
description: Optional[str] = None
|
|
44
|
-
commands: Optional[List[str]] = None
|
|
45
|
-
services: Optional[List[str]] = None
|
|
46
|
-
dependencies: Optional[List[str]] = None
|
|
47
|
-
|
|
48
|
-
import sys, fcntl, os, shlex, queue, subprocess as sp
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
# Simple approach to capture output in real-time
|
|
52
|
-
async def run_command_with_output(cmd, queue):
|
|
53
|
-
"""Run a command and capture output in real-time."""
|
|
54
|
-
print(f"Running command: {cmd}")
|
|
55
|
-
|
|
56
|
-
# Start the process
|
|
57
|
-
process = sp.Popen(
|
|
58
|
-
cmd,
|
|
59
|
-
stdout=sp.PIPE,
|
|
60
|
-
stderr=sp.PIPE,
|
|
61
|
-
text=True,
|
|
62
|
-
bufsize=1 # Line buffered
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
# Helper function to read from a pipe and put lines in the queue
|
|
66
|
-
async def read_pipe(pipe, is_stderr=False):
|
|
67
|
-
for line in iter(pipe.readline, ''):
|
|
68
|
-
if not line: # Empty line means EOF
|
|
69
|
-
break
|
|
70
|
-
line = line.strip()
|
|
71
|
-
if line:
|
|
72
|
-
if is_stderr:
|
|
73
|
-
if "A new release of pip is available" in line or line.startswith("WARNING:"):
|
|
74
|
-
await queue.put(("warning", line))
|
|
75
|
-
else:
|
|
76
|
-
await queue.put(("error", line))
|
|
77
|
-
else:
|
|
78
|
-
await queue.put(("message", line))
|
|
79
|
-
await asyncio.sleep(0.01) # Small delay to allow other tasks to run
|
|
80
|
-
|
|
81
|
-
# Create tasks to read from stdout and stderr
|
|
82
|
-
stdout_task = asyncio.create_task(read_pipe(process.stdout))
|
|
83
|
-
stderr_task = asyncio.create_task(read_pipe(process.stderr, True))
|
|
84
|
-
|
|
85
|
-
# Wait for the process to complete
|
|
86
|
-
return_code = await asyncio.get_event_loop().run_in_executor(None, process.wait)
|
|
87
|
-
|
|
88
|
-
# Wait for the reading tasks to complete
|
|
89
|
-
await stdout_task
|
|
90
|
-
await stderr_task
|
|
91
|
-
|
|
92
|
-
# Close the pipes
|
|
93
|
-
process.stdout.close()
|
|
94
|
-
process.stderr.close()
|
|
95
|
-
|
|
96
|
-
# Return the return code
|
|
97
|
-
return return_code
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
# Generator for SSE events
|
|
101
|
-
async def stream_command_output(cmd, message):
|
|
102
|
-
"""Stream command output as SSE events."""
|
|
103
|
-
# Send initial message
|
|
104
|
-
yield {"event": "message", "data": message}
|
|
105
|
-
|
|
106
|
-
# Create a queue for output
|
|
107
|
-
output_queue = asyncio.Queue()
|
|
108
|
-
|
|
109
|
-
# Run the command and capture output
|
|
110
|
-
return_code = await run_command_with_output(cmd, output_queue)
|
|
111
|
-
|
|
112
|
-
# Stream output from the queue
|
|
113
|
-
while not output_queue.empty():
|
|
114
|
-
event_type, data = await output_queue.get()
|
|
115
|
-
yield {"event": event_type, "data": data}
|
|
116
|
-
|
|
117
|
-
# Send completion message
|
|
118
|
-
if return_code == 0:
|
|
119
|
-
yield {"event": "complete", "data": "Installation completed successfully"}
|
|
120
|
-
else:
|
|
121
|
-
yield {"event": "error", "data": "Installation failed"}
|
|
122
|
-
|
|
123
|
-
import sys, io, subprocess
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
async def stream_install_generator(plugin_name, source, source_path=None, remote_source=None):
|
|
127
|
-
"""Generator for streaming plugin installation output."""
|
|
128
|
-
# Create a subprocess to capture output
|
|
129
|
-
import subprocess
|
|
130
|
-
import sys
|
|
131
|
-
import io
|
|
132
|
-
from contextlib import redirect_stdout, redirect_stderr
|
|
133
|
-
import threading, select
|
|
134
|
-
|
|
135
|
-
# Send initial message
|
|
136
|
-
yield {"event": "message", "data": f"Starting installation of {plugin_name} from {source}..."}
|
|
137
|
-
|
|
138
|
-
# Create string buffers to capture output
|
|
139
|
-
stdout_buffer = io.StringIO()
|
|
140
|
-
stderr_buffer = io.StringIO()
|
|
141
|
-
output_queue = []
|
|
142
|
-
installation_complete = threading.Event()
|
|
143
|
-
installation_success = [False] # Use a list to make it mutable in the inner function
|
|
144
|
-
import time, fcntl, os, shlex, queue, subprocess as sp
|
|
145
|
-
|
|
146
|
-
# Helper function to read from pipes without blocking
|
|
147
|
-
def read_pipes_nonblocking(process):
|
|
148
|
-
stdout_data = []
|
|
149
|
-
stderr_data = []
|
|
150
|
-
|
|
151
|
-
# Set pipes to non-blocking mode
|
|
152
|
-
if not process.stdout or not process.stderr:
|
|
153
|
-
return stdout_data, stderr_data
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
# Set stdout to non-blocking
|
|
157
|
-
stdout_fd = process.stdout.fileno()
|
|
158
|
-
fl = fcntl.fcntl(stdout_fd, fcntl.F_GETFL)
|
|
159
|
-
fcntl.fcntl(stdout_fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
|
|
160
|
-
|
|
161
|
-
# Set stderr to non-blocking
|
|
162
|
-
stderr_fd = process.stderr.fileno()
|
|
163
|
-
fl = fcntl.fcntl(stderr_fd, fcntl.F_GETFL)
|
|
164
|
-
fcntl.fcntl(stderr_fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
|
|
165
|
-
|
|
166
|
-
# Read from pipes
|
|
167
|
-
try:
|
|
168
|
-
readable, _, _ = select.select([process.stdout, process.stderr], [], [], 0.1)
|
|
169
|
-
except (ValueError, select.error):
|
|
170
|
-
return stdout_data, stderr_data
|
|
171
|
-
|
|
172
|
-
if process.stdout in readable:
|
|
173
|
-
try:
|
|
174
|
-
stdout_line = process.stdout.readline()
|
|
175
|
-
if stdout_line:
|
|
176
|
-
stdout_data.append(stdout_line.strip())
|
|
177
|
-
except IOError:
|
|
178
|
-
pass
|
|
179
|
-
|
|
180
|
-
if process.stderr in readable:
|
|
181
|
-
try:
|
|
182
|
-
stderr_line = process.stderr.readline()
|
|
183
|
-
if stderr_line:
|
|
184
|
-
stderr_data.append(stderr_line.strip())
|
|
185
|
-
except IOError:
|
|
186
|
-
pass
|
|
187
|
-
|
|
188
|
-
return stdout_data, stderr_data
|
|
189
|
-
|
|
190
|
-
# Helper function to determine if a stderr line is an actual error or just a warning/notice
|
|
191
|
-
def is_actual_error(line):
|
|
192
|
-
# Pip upgrade notices are not actual errors
|
|
193
|
-
if "A new release of pip is available" in line:
|
|
194
|
-
return False
|
|
195
|
-
if "pip is being invoked by an old script wrapper" in line:
|
|
196
|
-
return False
|
|
197
|
-
# Add other known non-error stderr messages here
|
|
198
|
-
if line.startswith("WARNING:") or line.startswith("DEPRECATION:"):
|
|
199
|
-
return False
|
|
200
|
-
# Consider everything else as an error
|
|
201
|
-
return True
|
|
202
|
-
|
|
203
|
-
# Capture output directly using subprocess.check_output
|
|
204
|
-
# Thread function to continuously read from a pipe
|
|
205
|
-
def reader_thread(pipe, queue, is_stderr=False, stderr_lines=None):
|
|
206
|
-
try:
|
|
207
|
-
with pipe:
|
|
208
|
-
for line in iter(pipe.readline, ''):
|
|
209
|
-
if not line: # Empty line means EOF
|
|
210
|
-
break
|
|
211
|
-
line = line.strip()
|
|
212
|
-
if is_stderr:
|
|
213
|
-
# Always add stderr lines to the all_stderr_lines list
|
|
214
|
-
if stderr_lines is not None:
|
|
215
|
-
stderr_lines.append(line)
|
|
216
|
-
|
|
217
|
-
if is_actual_error(line):
|
|
218
|
-
print(f"ERROR LINE: {line}") # Debug
|
|
219
|
-
queue.put(("error", line))
|
|
220
|
-
else:
|
|
221
|
-
print(f"WARNING LINE: {line}") # Debug
|
|
222
|
-
queue.put(("warning", line))
|
|
223
|
-
else:
|
|
224
|
-
queue.put(("message", line))
|
|
225
|
-
except Exception as e:
|
|
226
|
-
queue.put(("error", f"Error reading from {'stderr' if is_stderr else 'stdout'}: {str(e)}"))
|
|
227
|
-
finally:
|
|
228
|
-
if is_stderr:
|
|
229
|
-
queue.put(("reader_done", "stderr"))
|
|
230
|
-
else:
|
|
231
|
-
queue.put(("reader_done", "stdout"))
|
|
232
|
-
|
|
233
|
-
# Helper function to check if a line contains meaningful output
|
|
234
|
-
def is_meaningful_output(line):
|
|
235
|
-
return bool(line and line.strip() and not line.isspace())
|
|
236
|
-
|
|
237
|
-
# Helper function to run a pip command and capture output
|
|
238
|
-
def run_pip_command(cmd, message):
|
|
239
|
-
output_queue.append(("message", message))
|
|
240
|
-
|
|
241
|
-
all_stderr_lines = []
|
|
242
|
-
|
|
243
|
-
# Make all_stderr_lines accessible to the reader_thread function
|
|
244
|
-
# Use shell=True for better output capture on some systems
|
|
245
|
-
if isinstance(cmd, list):
|
|
246
|
-
cmd_str = ' '.join(shlex.quote(str(arg)) for arg in cmd)
|
|
247
|
-
else:
|
|
248
|
-
cmd_str = cmd
|
|
249
|
-
|
|
250
|
-
process = subprocess.Popen(
|
|
251
|
-
# Use shell=True to get more verbose output
|
|
252
|
-
cmd_str,
|
|
253
|
-
stdout=subprocess.PIPE,
|
|
254
|
-
stderr=subprocess.PIPE,
|
|
255
|
-
text=True,
|
|
256
|
-
shell=True,
|
|
257
|
-
bufsize=1 # Line buffered
|
|
258
|
-
)
|
|
259
|
-
|
|
260
|
-
output_queue.append(("message", f"Running: {cmd_str}"))
|
|
261
|
-
|
|
262
|
-
# Create a queue for thread communication
|
|
263
|
-
q = queue.Queue()
|
|
264
|
-
|
|
265
|
-
# Start reader threads
|
|
266
|
-
stdout_thread = threading.Thread(target=reader_thread, args=(process.stdout, q, False, None))
|
|
267
|
-
stderr_thread = threading.Thread(target=reader_thread, args=(process.stderr, q, True, all_stderr_lines))
|
|
268
|
-
stdout_thread.daemon = True
|
|
269
|
-
stderr_thread.daemon = True
|
|
270
|
-
stdout_thread.start()
|
|
271
|
-
stderr_thread.start()
|
|
272
|
-
|
|
273
|
-
# Track which readers are done
|
|
274
|
-
readers_done = set()
|
|
275
|
-
|
|
276
|
-
# Read output in real-time using non-blocking approach
|
|
277
|
-
# Process queue items until both readers are done and process has exited
|
|
278
|
-
while len(readers_done) < 2 or process.poll() is None:
|
|
279
|
-
try:
|
|
280
|
-
item_type, item = q.get(timeout=0.1)
|
|
281
|
-
|
|
282
|
-
if item_type == "reader_done":
|
|
283
|
-
readers_done.add(item) # item will be "stdout" or "stderr"
|
|
284
|
-
elif item_type == "error":
|
|
285
|
-
all_stderr_lines.append(item)
|
|
286
|
-
output_queue.append(("error", item))
|
|
287
|
-
# Send error event
|
|
288
|
-
yield {"event": "error", "data": item}
|
|
289
|
-
elif item_type == "warning":
|
|
290
|
-
all_stderr_lines.append(item)
|
|
291
|
-
output_queue.append(("warning", item))
|
|
292
|
-
# Send warning event
|
|
293
|
-
yield {"event": "warning", "data": item}
|
|
294
|
-
else: # message
|
|
295
|
-
if is_meaningful_output(item):
|
|
296
|
-
output_queue.append(("message", item))
|
|
297
|
-
# Send message event
|
|
298
|
-
yield {"event": "message", "data": item}
|
|
299
|
-
|
|
300
|
-
q.task_done()
|
|
301
|
-
except queue.Empty:
|
|
302
|
-
# No output available, just continue
|
|
303
|
-
pass
|
|
304
|
-
|
|
305
|
-
# Wait for threads to finish
|
|
306
|
-
stdout_thread.join()
|
|
307
|
-
stderr_thread.join()
|
|
308
|
-
|
|
309
|
-
# If the process failed but only had warnings (not actual errors), consider it a success
|
|
310
|
-
if process.returncode != 0:
|
|
311
|
-
# Debug
|
|
312
|
-
print(f"All stderr lines: {all_stderr_lines}")
|
|
313
|
-
print(f"Output queue: {output_queue}")
|
|
314
|
-
|
|
315
|
-
# Check if there were any actual errors in stderr
|
|
316
|
-
has_actual_errors = any(is_actual_error(line) for line in all_stderr_lines)
|
|
317
|
-
print(f"Process returned {process.returncode}, has_actual_errors={has_actual_errors}")
|
|
318
|
-
|
|
319
|
-
# If no actual errors, consider it a success
|
|
320
|
-
if not has_actual_errors:
|
|
321
|
-
return 0
|
|
322
|
-
|
|
323
|
-
# Add a final message with the return code
|
|
324
|
-
output_queue.append(("message", f"Process completed with return code: {process.returncode}"))
|
|
325
|
-
yield {"event": "message", "data": f"Process completed with return code: {process.returncode}"}
|
|
326
|
-
|
|
327
|
-
success_status = process.returncode == 0 or not any(is_actual_error(line) for line in all_stderr_lines)
|
|
328
|
-
|
|
329
|
-
# Add a final success/error message
|
|
330
|
-
if success_status:
|
|
331
|
-
yield {"event": "complete", "data": "Installation completed successfully"}
|
|
332
|
-
else:
|
|
333
|
-
yield {"event": "error", "data": "Installation failed"}
|
|
334
|
-
|
|
335
|
-
# Return the process return code
|
|
336
|
-
return process.returncode
|
|
337
|
-
|
|
338
|
-
# Function to run pip install and capture output
|
|
339
|
-
def run_pip_install():
|
|
340
|
-
try:
|
|
341
|
-
if source == 'github':
|
|
342
|
-
# First download the GitHub repo
|
|
343
|
-
output_queue.append(("message", f"Downloading GitHub repository {source_path}..."))
|
|
344
|
-
|
|
345
|
-
# Install from GitHub
|
|
346
|
-
cmd = [sys.executable, '-m', 'pip', 'install', '-e', f"{source_path}", '-v']
|
|
347
|
-
return_code = run_pip_command(cmd, f"Installing {plugin_name} from GitHub repository {source_path}...")
|
|
348
|
-
installation_success[0] = (return_code == 0)
|
|
349
|
-
|
|
350
|
-
elif source == 'local':
|
|
351
|
-
# Run pip install for local source
|
|
352
|
-
cmd = [sys.executable, '-m', 'pip', 'install', '-e', source_path, '-v']
|
|
353
|
-
return_code = run_pip_command(cmd, f"Installing from local path: {source_path}")
|
|
354
|
-
installation_success[0] = (return_code == 0)
|
|
355
|
-
print(f"Local installation result: return_code={return_code}, success={installation_success[0]}")
|
|
356
|
-
elif source == 'pypi':
|
|
357
|
-
# Run pip install for PyPI package
|
|
358
|
-
cmd = [sys.executable, '-m', 'pip', 'install', plugin_name, '-v']
|
|
359
|
-
return_code = run_pip_command(cmd, f"Installing from PyPI: {plugin_name}")
|
|
360
|
-
installation_success[0] = (return_code == 0)
|
|
361
|
-
print(f"PyPI installation result: return_code={return_code}, success={installation_success[0]}")
|
|
362
|
-
installation_complete.set()
|
|
363
|
-
except Exception as e:
|
|
364
|
-
trace = traceback.format_exc()
|
|
365
|
-
output_queue.append(("error", f"Error installing plugin: {str(e)}"))
|
|
366
|
-
for line in trace.splitlines():
|
|
367
|
-
output_queue.append(("error", line))
|
|
368
|
-
installation_complete.set()
|
|
369
|
-
|
|
370
|
-
try:
|
|
371
|
-
# Start the installation process
|
|
372
|
-
thread = threading.Thread(target=run_pip_install)
|
|
373
|
-
thread.start()
|
|
374
|
-
|
|
375
|
-
# Stream output from the queue while waiting for installation to complete
|
|
376
|
-
while not installation_complete.is_set() or output_queue:
|
|
377
|
-
if output_queue:
|
|
378
|
-
event_type, data = output_queue.pop(0)
|
|
379
|
-
yield {"event": event_type, "data": data}
|
|
380
|
-
else:
|
|
381
|
-
await asyncio.sleep(0.1)
|
|
382
|
-
|
|
383
|
-
# Send completion message
|
|
384
|
-
if installation_success[0]:
|
|
385
|
-
yield {"event": "complete", "data": f"Plugin {plugin_name} installed successfully"}
|
|
386
|
-
else:
|
|
387
|
-
yield {"event": "error", "data": f"Failed to install plugin {plugin_name}"}
|
|
388
|
-
|
|
389
|
-
except Exception as e:
|
|
390
|
-
trace = traceback.format_exc()
|
|
391
|
-
yield {"event": "error", "data": f"Error installing plugin: {str(e)}"}
|
|
392
|
-
for line in trace.splitlines():
|
|
393
|
-
yield {"event": "error", "data": line}
|
|
394
|
-
|
|
395
|
-
@router.post("/stream-install-plugin", response_class=EventSourceResponse)
|
|
396
|
-
async def stream_install_plugin(request: StreamInstallRequest):
|
|
397
|
-
"""Stream the installation process of a plugin using SSE."""
|
|
398
|
-
return EventSourceResponse(stream_install_generator(
|
|
399
|
-
request.plugin, request.source, request.source_path))
|
|
400
|
-
|
|
401
|
-
@router.get("/stream-install-plugin", response_class=EventSourceResponse)
|
|
402
|
-
async def stream_install_plugin_get(request: Request):
|
|
403
|
-
"""Stream the installation process of a plugin using SSE (GET method)."""
|
|
404
|
-
# Extract parameters from query string
|
|
405
|
-
plugin = request.query_params.get("plugin", "")
|
|
406
|
-
source = request.query_params.get("source", "")
|
|
407
|
-
source_path = request.query_params.get("source_path", "")
|
|
408
|
-
|
|
409
|
-
# Use the new simpler approach
|
|
410
|
-
if source == 'github':
|
|
411
|
-
cmd = [sys.executable, '-m', 'pip', 'install', '-e', source_path, '-v']
|
|
412
|
-
message = f"Installing {plugin} from GitHub repository {source_path}..."
|
|
413
|
-
elif source == 'local':
|
|
414
|
-
cmd = [sys.executable, '-m', 'pip', 'install', '-e', source_path, '-v']
|
|
415
|
-
message = f"Installing from local path: {source_path}"
|
|
416
|
-
elif source == 'pypi':
|
|
417
|
-
cmd = [sys.executable, '-m', 'pip', 'install', plugin, '-v']
|
|
418
|
-
message = f"Installing from PyPI: {plugin}"
|
|
419
|
-
else:
|
|
420
|
-
return {"success": False, "message": "Invalid source"}
|
|
421
|
-
|
|
422
|
-
# Return SSE response
|
|
423
|
-
return EventSourceResponse(stream_command_output(cmd, message))
|
|
424
|
-
|
|
425
|
-
@router.get("/get-all-plugins")
|
|
426
|
-
async def get_all_plugins():
|
|
427
|
-
try:
|
|
428
|
-
manifest = load_plugin_manifest()
|
|
429
|
-
plugins = []
|
|
430
|
-
|
|
431
|
-
# Process core plugins
|
|
432
|
-
for plugin_name, plugin_info in manifest['plugins']['core'].items():
|
|
433
|
-
plugins.append({
|
|
434
|
-
"name": plugin_name,
|
|
435
|
-
"category": "core",
|
|
436
|
-
"enabled": plugin_info['enabled'],
|
|
437
|
-
"source": "core",
|
|
438
|
-
"remote_source": plugin_name,
|
|
439
|
-
"version": "1.0.0",
|
|
440
|
-
"description": plugin_info.get('metadata', {}).get('description', '')
|
|
441
|
-
})
|
|
442
|
-
|
|
443
|
-
# Process installed plugins
|
|
444
|
-
for plugin_name, plugin_info in manifest['plugins']['installed'].items():
|
|
445
|
-
plugins.append({
|
|
446
|
-
"name": plugin_name,
|
|
447
|
-
"category": "installed",
|
|
448
|
-
"enabled": plugin_info['enabled'],
|
|
449
|
-
"source": plugin_info['source'],
|
|
450
|
-
"remote_source": plugin_info.get('remote_source', plugin_info.get('github_url')),
|
|
451
|
-
"source_path": plugin_info.get('source_path'),
|
|
452
|
-
"version": plugin_info.get('version', '0.0.1'),
|
|
453
|
-
"description": plugin_info.get('metadata', {}).get('description', ''),
|
|
454
|
-
"index_source": plugin_info.get('metadata', {}).get('index_source')
|
|
455
|
-
})
|
|
456
|
-
|
|
457
|
-
return {"success": True, "data": plugins}
|
|
458
|
-
except Exception as e:
|
|
459
|
-
trace = traceback.format_exc()
|
|
460
|
-
return {"success": False, "message": f"Error fetching plugins: {str(e)}\n\n{trace}"}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
@router.post("/scan-directory")
|
|
464
|
-
async def scan_directory(request: DirectoryRequest):
|
|
465
|
-
try:
|
|
466
|
-
directory = request.directory
|
|
467
|
-
if not os.path.isdir(directory):
|
|
468
|
-
return {"success": False, "message": "Invalid directory path"}
|
|
469
|
-
|
|
470
|
-
discovered_plugins = discover_plugins(directory)
|
|
471
|
-
manifest = load_plugin_manifest()
|
|
472
|
-
print("discoverd_plugins", discovered_plugins)
|
|
473
|
-
# Update installed plugins from discovered ones
|
|
474
|
-
for plugin_name, plugin_info in discovered_plugins.items():
|
|
475
|
-
plugin_info['source'] = 'local'
|
|
476
|
-
plugin_info['metadata'] = plugin_info.get('metadata', {}) or {
|
|
477
|
-
"description": plugin_info.get('description', ''),
|
|
478
|
-
"install_date": plugin_info.get('install_date', ''),
|
|
479
|
-
"commands": plugin_info.get('commands', []),
|
|
480
|
-
"services": plugin_info.get('services', [])
|
|
481
|
-
}
|
|
482
|
-
print(plugin_info)
|
|
483
|
-
manifest['plugins']['installed'][plugin_name] = plugin_info
|
|
484
|
-
|
|
485
|
-
# Prepare plugin list for response
|
|
486
|
-
plugins_list = [{
|
|
487
|
-
"name": name,
|
|
488
|
-
"description": info.get('metadata', {}).get('description', info.get('description', ''))
|
|
489
|
-
} for name, info in discovered_plugins.items()]
|
|
490
|
-
|
|
491
|
-
save_plugin_manifest(manifest)
|
|
492
|
-
return {"success": True,
|
|
493
|
-
"message": f"Scanned {len(discovered_plugins)} plugins in {directory}",
|
|
494
|
-
"plugins": plugins_list}
|
|
495
|
-
except Exception as e:
|
|
496
|
-
trace = traceback.format_exc()
|
|
497
|
-
return {"success": False, "message": f"Error during scan: {str(e)}\n\n{trace}"}
|
|
498
|
-
|
|
499
|
-
@router.post("/install-local-plugin")
|
|
500
|
-
async def install_local_plugin(request: PluginRequest):
|
|
501
|
-
try:
|
|
502
|
-
plugin_name = request.plugin
|
|
503
|
-
plugin_path = get_plugin_path(plugin_name)
|
|
504
|
-
|
|
505
|
-
if not plugin_path:
|
|
506
|
-
return {"success": False, "message": "Plugin path not found"}
|
|
507
|
-
|
|
508
|
-
success = await plugin_install(plugin_name, source='local', source_path=plugin_path)
|
|
509
|
-
if success:
|
|
510
|
-
return {"success": True, "message": f"Plugin {plugin_name} installed successfully"}
|
|
511
|
-
else:
|
|
512
|
-
return {"success": False, "message": "Installation failed"}
|
|
513
|
-
except Exception as e:
|
|
514
|
-
trace = traceback.format_exc()
|
|
515
|
-
return {"success": False, "message": f"Error installing plugin: {str(e)}\n\n{trace}"}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
@router.post("/install-x-github-plugin")
|
|
519
|
-
async def install_github_plugin(request: GitHubPluginRequest):
|
|
520
|
-
try:
|
|
521
|
-
print("Request:", request)
|
|
522
|
-
url = request.url or request.github_url
|
|
523
|
-
success = await plugin_install('test', source='github', source_path=url)
|
|
524
|
-
if success:
|
|
525
|
-
return {"success": True, "message": "Plugin installed successfully from GitHub"}
|
|
526
|
-
else:
|
|
527
|
-
return {"success": False, "message": "Installation failed"}
|
|
528
|
-
except Exception as e:
|
|
529
|
-
trace = traceback.format_exc()
|
|
530
|
-
return {"success": False, "message": f"Error installing from GitHub: {str(e)}\n\n{trace}"}
|
|
531
|
-
|
|
532
|
-
@router.post("/install-from-index")
|
|
533
|
-
async def install_from_index(request: InstallFromIndexRequest):
|
|
534
|
-
try:
|
|
535
|
-
# Load the index to get plugin information
|
|
536
|
-
index_path = os.path.join('indices', f"{request.index_name}.json")
|
|
537
|
-
if not os.path.exists(index_path):
|
|
538
|
-
return {"success": False, "message": "Index not found"}
|
|
539
|
-
|
|
540
|
-
with open(index_path, 'r') as f:
|
|
541
|
-
index_data = json.load(f)
|
|
542
|
-
|
|
543
|
-
# Find plugin in index
|
|
544
|
-
plugin_data = None
|
|
545
|
-
for plugin in index_data.get('plugins', []):
|
|
546
|
-
if plugin['name'] == request.plugin:
|
|
547
|
-
plugin_data = plugin
|
|
548
|
-
break
|
|
549
|
-
|
|
550
|
-
if not plugin_data:
|
|
551
|
-
return {"success": False, "message": "Plugin not found in index"}
|
|
552
|
-
|
|
553
|
-
# Install the plugin
|
|
554
|
-
if plugin_data.get('github_url'):
|
|
555
|
-
success = await plugin_install(
|
|
556
|
-
request.plugin,
|
|
557
|
-
source='github',
|
|
558
|
-
source_path=plugin_data['github_url']
|
|
559
|
-
)
|
|
560
|
-
elif plugin_data.get('source_path'):
|
|
561
|
-
success = await plugin_install(
|
|
562
|
-
request.plugin,
|
|
563
|
-
source='local',
|
|
564
|
-
source_path=plugin_data['source_path']
|
|
565
|
-
)
|
|
566
|
-
else:
|
|
567
|
-
return {"success": False, "message": "No valid installation source in index"}
|
|
568
|
-
|
|
569
|
-
if success:
|
|
570
|
-
# Update plugin metadata with index information
|
|
571
|
-
manifest = load_plugin_manifest()
|
|
572
|
-
if request.plugin in manifest['plugins']['installed']:
|
|
573
|
-
manifest['plugins']['installed'][request.plugin]['metadata']['index_source'] = request.index_name
|
|
574
|
-
save_plugin_manifest(manifest)
|
|
575
|
-
|
|
576
|
-
return {"success": True, "message": f"Plugin {request.plugin} installed successfully from index"}
|
|
577
|
-
else:
|
|
578
|
-
return {"success": False, "message": "Installation failed"}
|
|
579
|
-
|
|
580
|
-
except Exception as e:
|
|
581
|
-
trace = traceback.format_exc()
|
|
582
|
-
return {"success": False, "message": f"Error installing from index: {str(e)}\n\n{trace}"}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
@router.post("/toggle-plugin")
|
|
586
|
-
async def toggle_plugin(request: TogglePluginRequest):
|
|
587
|
-
try:
|
|
588
|
-
success = toggle_plugin_state(request.plugin, request.enabled)
|
|
589
|
-
if success:
|
|
590
|
-
return {"success": True, "message": f"Plugin {request.plugin} {'enabled' if request.enabled else 'disabled'} successfully"}
|
|
591
|
-
else:
|
|
592
|
-
return {"success": False, "message": "Failed to toggle plugin state"}
|
|
593
|
-
except Exception as e:
|
|
594
|
-
trace = traceback.format_exc()
|
|
595
|
-
return {"success": False, "message": f"Error toggling plugin: {str(e)}\n\n{trace}"}
|
|
596
|
-
|
|
597
|
-
# Helper function
|
|
598
|
-
def discover_plugins(directory):
|
|
599
|
-
discovered = {}
|
|
600
|
-
for item in os.listdir(directory):
|
|
601
|
-
item_path = os.path.join(directory, item)
|
|
602
|
-
plugin_info_path = os.path.join(item_path, 'plugin_info.json')
|
|
603
|
-
|
|
604
|
-
if os.path.isfile(plugin_info_path):
|
|
605
|
-
try:
|
|
606
|
-
with open(plugin_info_path, 'r') as f:
|
|
607
|
-
plugin_info = json.load(f)
|
|
608
|
-
plugin_info['enabled'] = False
|
|
609
|
-
plugin_info['source_path'] = item_path
|
|
610
|
-
discovered[plugin_info['name']] = plugin_info
|
|
611
|
-
except json.JSONDecodeError:
|
|
612
|
-
print(f"Error reading plugin info for {item}")
|
|
613
|
-
continue
|
|
614
|
-
|
|
615
|
-
return discovered
|