loki-mode 6.44.1 → 6.45.0
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.
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/loki +30 -18
- package/dashboard/__init__.py +1 -1
- package/docs/INSTALLATION.md +1 -1
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/web-app/dist/assets/{Badge-DTFBYZoA.js → Badge-D1-tb3Ci.js} +1 -1
- package/web-app/dist/assets/Button-DUExR4iB.js +6 -0
- package/web-app/dist/assets/{Card-CL6xo_RX.js → Card-LXXTWpme.js} +1 -1
- package/web-app/dist/assets/{HomePage-1Ccs1i0m.js → HomePage-C3QLIWSC.js} +1 -1
- package/web-app/dist/assets/ProjectPage-CkFeNhXf.js +162 -0
- package/web-app/dist/assets/{ProjectsPage-DEP-ZiW7.js → ProjectsPage-CzyuQptX.js} +2 -7
- package/web-app/dist/assets/{SettingsPage-CULNMl6W.js → SettingsPage-BwpQIkqM.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-DdrBEy9w.js → TemplatesPage-D5h6cG1N.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-C3fQIXqq.js → TerminalOutput-CnXPLmFM.js} +1 -1
- package/web-app/dist/assets/{clock-D5NTAxeP.js → clock-DRfchEI9.js} +1 -1
- package/web-app/dist/assets/{external-link-CSO4H-LC.js → external-link-CS_9f84I.js} +1 -1
- package/web-app/dist/assets/index-BZpAV5M8.css +1 -0
- package/web-app/dist/assets/{index-DvhjaUe4.js → index-HocOQZ9L.js} +17 -17
- package/web-app/dist/index.html +2 -2
- package/web-app/server.py +327 -61
- package/web-app/dist/assets/Button-NSmP6YCA.js +0 -1
- package/web-app/dist/assets/ProjectPage-wby1itP1.js +0 -141
- package/web-app/dist/assets/index-BLBd5LBk.css +0 -1
package/web-app/dist/index.html
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
9
9
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
10
10
|
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display&family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
|
11
|
-
<script type="module" crossorigin src="/assets/index-
|
|
12
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
11
|
+
<script type="module" crossorigin src="/assets/index-HocOQZ9L.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BZpAV5M8.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body class="bg-background text-ink font-sans antialiased">
|
|
15
15
|
<div id="root"></div>
|
package/web-app/server.py
CHANGED
|
@@ -12,10 +12,12 @@ import asyncio
|
|
|
12
12
|
import inspect
|
|
13
13
|
import json
|
|
14
14
|
import os
|
|
15
|
+
import re
|
|
15
16
|
import signal
|
|
16
17
|
import subprocess
|
|
17
18
|
import sys
|
|
18
19
|
import time
|
|
20
|
+
import uuid
|
|
19
21
|
from pathlib import Path
|
|
20
22
|
from typing import Optional
|
|
21
23
|
|
|
@@ -109,46 +111,85 @@ class SessionState:
|
|
|
109
111
|
self.log_lines = []
|
|
110
112
|
|
|
111
113
|
|
|
112
|
-
def
|
|
113
|
-
"""Kill
|
|
114
|
+
def _kill_tracked_child_processes() -> None:
|
|
115
|
+
"""Kill only processes that Purple Lab started, not external loki sessions."""
|
|
114
116
|
import subprocess as _sp
|
|
115
|
-
|
|
116
|
-
if
|
|
117
|
-
|
|
118
|
-
patterns.append("loki-run-")
|
|
117
|
+
tracked = _get_tracked_child_pids()
|
|
118
|
+
if not tracked:
|
|
119
|
+
return
|
|
119
120
|
|
|
120
|
-
|
|
121
|
-
for pattern in patterns:
|
|
121
|
+
for pid in tracked:
|
|
122
122
|
try:
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
for pid_str in result.stdout.strip().splitlines():
|
|
129
|
-
try:
|
|
130
|
-
pid = int(pid_str.strip())
|
|
131
|
-
if pid not in killed_pids:
|
|
132
|
-
os.kill(pid, signal.SIGTERM)
|
|
133
|
-
killed_pids.add(pid)
|
|
134
|
-
except (ValueError, ProcessLookupError, PermissionError):
|
|
135
|
-
pass
|
|
136
|
-
except Exception:
|
|
123
|
+
# Kill the entire process tree (children first, then parent)
|
|
124
|
+
_sp.run(["pkill", "-TERM", "-P", str(pid)],
|
|
125
|
+
capture_output=True, timeout=5)
|
|
126
|
+
os.kill(pid, signal.SIGTERM)
|
|
127
|
+
except (ProcessLookupError, PermissionError, OSError):
|
|
137
128
|
pass
|
|
138
129
|
|
|
139
|
-
# Wait briefly then SIGKILL
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
130
|
+
# Wait briefly then SIGKILL survivors
|
|
131
|
+
import time as _time
|
|
132
|
+
_time.sleep(2)
|
|
133
|
+
for pid in tracked:
|
|
134
|
+
try:
|
|
135
|
+
_sp.run(["pkill", "-9", "-P", str(pid)],
|
|
136
|
+
capture_output=True, timeout=5)
|
|
137
|
+
os.kill(pid, signal.SIGKILL)
|
|
138
|
+
except (ProcessLookupError, PermissionError, OSError):
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
_clear_tracked_pids()
|
|
148
142
|
|
|
149
143
|
|
|
150
144
|
session = SessionState()
|
|
151
145
|
|
|
146
|
+
# Track PIDs of sessions started by Purple Lab (not by external loki CLI)
|
|
147
|
+
_PURPLE_LAB_PIDS_FILE = SCRIPT_DIR.parent / ".loki" / "purple-lab" / "child-pids.json"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _track_child_pid(pid: int) -> None:
|
|
151
|
+
"""Record a PID started by Purple Lab so loki web stop can clean it up."""
|
|
152
|
+
_PURPLE_LAB_PIDS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
153
|
+
pids: list[int] = []
|
|
154
|
+
if _PURPLE_LAB_PIDS_FILE.exists():
|
|
155
|
+
try:
|
|
156
|
+
pids = json.loads(_PURPLE_LAB_PIDS_FILE.read_text())
|
|
157
|
+
except (json.JSONDecodeError, OSError):
|
|
158
|
+
pids = []
|
|
159
|
+
if pid not in pids:
|
|
160
|
+
pids.append(pid)
|
|
161
|
+
_PURPLE_LAB_PIDS_FILE.write_text(json.dumps(pids))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _untrack_child_pid(pid: int) -> None:
|
|
165
|
+
"""Remove a PID from tracking after it exits."""
|
|
166
|
+
if not _PURPLE_LAB_PIDS_FILE.exists():
|
|
167
|
+
return
|
|
168
|
+
try:
|
|
169
|
+
pids = json.loads(_PURPLE_LAB_PIDS_FILE.read_text())
|
|
170
|
+
pids = [p for p in pids if p != pid]
|
|
171
|
+
_PURPLE_LAB_PIDS_FILE.write_text(json.dumps(pids))
|
|
172
|
+
except (json.JSONDecodeError, OSError):
|
|
173
|
+
pass
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _get_tracked_child_pids() -> list[int]:
|
|
177
|
+
"""Get all PIDs started by Purple Lab."""
|
|
178
|
+
if not _PURPLE_LAB_PIDS_FILE.exists():
|
|
179
|
+
return []
|
|
180
|
+
try:
|
|
181
|
+
return json.loads(_PURPLE_LAB_PIDS_FILE.read_text())
|
|
182
|
+
except (json.JSONDecodeError, OSError):
|
|
183
|
+
return []
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _clear_tracked_pids() -> None:
|
|
187
|
+
"""Clear all tracked PIDs."""
|
|
188
|
+
try:
|
|
189
|
+
_PURPLE_LAB_PIDS_FILE.unlink(missing_ok=True)
|
|
190
|
+
except OSError:
|
|
191
|
+
pass
|
|
192
|
+
|
|
152
193
|
# ---------------------------------------------------------------------------
|
|
153
194
|
# Request / Response models
|
|
154
195
|
# ---------------------------------------------------------------------------
|
|
@@ -203,6 +244,11 @@ class ChatRequest(BaseModel):
|
|
|
203
244
|
message: str
|
|
204
245
|
mode: str = "quick" # "quick" or "standard"
|
|
205
246
|
|
|
247
|
+
|
|
248
|
+
class SecretRequest(BaseModel):
|
|
249
|
+
key: str
|
|
250
|
+
value: str
|
|
251
|
+
|
|
206
252
|
# ---------------------------------------------------------------------------
|
|
207
253
|
# Helpers
|
|
208
254
|
# ---------------------------------------------------------------------------
|
|
@@ -328,6 +374,48 @@ def _build_file_tree(root: Path, max_depth: int = 4, _depth: int = 0) -> list[di
|
|
|
328
374
|
entries.append(node)
|
|
329
375
|
return entries
|
|
330
376
|
|
|
377
|
+
|
|
378
|
+
# ---------------------------------------------------------------------------
|
|
379
|
+
# Secrets management (plaintext -- this is a local dev tool, not a vault)
|
|
380
|
+
# ---------------------------------------------------------------------------
|
|
381
|
+
|
|
382
|
+
_SECRETS_FILE = SCRIPT_DIR.parent / ".loki" / "purple-lab" / "secrets.json"
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _load_secrets() -> dict[str, str]:
|
|
386
|
+
"""Load secrets from disk."""
|
|
387
|
+
if _SECRETS_FILE.exists():
|
|
388
|
+
try:
|
|
389
|
+
data = json.loads(_SECRETS_FILE.read_text())
|
|
390
|
+
if isinstance(data, dict):
|
|
391
|
+
return data
|
|
392
|
+
except (json.JSONDecodeError, OSError):
|
|
393
|
+
pass
|
|
394
|
+
return {}
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _save_secrets(secrets: dict[str, str]) -> None:
|
|
398
|
+
"""Save secrets to disk. WARNING: stored in plaintext."""
|
|
399
|
+
_SECRETS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
400
|
+
_SECRETS_FILE.write_text(json.dumps(secrets, indent=2))
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
# ---------------------------------------------------------------------------
|
|
404
|
+
# Chat task tracking (non-blocking chat via polling)
|
|
405
|
+
# ---------------------------------------------------------------------------
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
class ChatTask:
|
|
409
|
+
def __init__(self) -> None:
|
|
410
|
+
self.id = str(uuid.uuid4())[:8]
|
|
411
|
+
self.output_lines: list[str] = []
|
|
412
|
+
self.complete = False
|
|
413
|
+
self.returncode: int = -1
|
|
414
|
+
self.files_changed: list[str] = []
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
_chat_tasks: dict[str, ChatTask] = {}
|
|
418
|
+
|
|
331
419
|
# ---------------------------------------------------------------------------
|
|
332
420
|
# API endpoints
|
|
333
421
|
# ---------------------------------------------------------------------------
|
|
@@ -378,6 +466,10 @@ async def start_session(req: StartRequest) -> JSONResponse:
|
|
|
378
466
|
]
|
|
379
467
|
|
|
380
468
|
try:
|
|
469
|
+
# Load secrets and inject as env vars
|
|
470
|
+
build_env = {**os.environ, "LOKI_DIR": os.path.join(project_dir, ".loki")}
|
|
471
|
+
build_env.update(_load_secrets())
|
|
472
|
+
|
|
381
473
|
proc = subprocess.Popen(
|
|
382
474
|
cmd,
|
|
383
475
|
stdout=subprocess.PIPE,
|
|
@@ -385,7 +477,7 @@ async def start_session(req: StartRequest) -> JSONResponse:
|
|
|
385
477
|
stdin=subprocess.DEVNULL,
|
|
386
478
|
text=True,
|
|
387
479
|
cwd=project_dir,
|
|
388
|
-
env=
|
|
480
|
+
env=build_env,
|
|
389
481
|
**({"start_new_session": True} if sys.platform != "win32"
|
|
390
482
|
else {"creationflags": subprocess.CREATE_NEW_PROCESS_GROUP}),
|
|
391
483
|
)
|
|
@@ -409,6 +501,9 @@ async def start_session(req: StartRequest) -> JSONResponse:
|
|
|
409
501
|
session.project_dir = project_dir
|
|
410
502
|
session.start_time = time.time()
|
|
411
503
|
|
|
504
|
+
# Track this PID so loki web stop knows it's ours
|
|
505
|
+
_track_child_pid(proc.pid)
|
|
506
|
+
|
|
412
507
|
# Start background output reader
|
|
413
508
|
session._reader_task = asyncio.create_task(_read_process_output())
|
|
414
509
|
|
|
@@ -485,7 +580,7 @@ async def stop_session() -> JSONResponse:
|
|
|
485
580
|
# Kill any orphaned loki-run processes for this project
|
|
486
581
|
if session.project_dir:
|
|
487
582
|
await asyncio.get_running_loop().run_in_executor(
|
|
488
|
-
None,
|
|
583
|
+
None, _kill_tracked_child_processes
|
|
489
584
|
)
|
|
490
585
|
|
|
491
586
|
await _broadcast({"type": "session_end", "data": {"message": "Session stopped by user"}})
|
|
@@ -1558,44 +1653,61 @@ async def onboard_session(req: OnboardRequest) -> JSONResponse:
|
|
|
1558
1653
|
|
|
1559
1654
|
@app.post("/api/sessions/{session_id}/chat")
|
|
1560
1655
|
async def chat_session(session_id: str, req: ChatRequest) -> JSONResponse:
|
|
1561
|
-
"""
|
|
1562
|
-
import re
|
|
1656
|
+
"""Start a chat command (non-blocking). Returns task_id for polling."""
|
|
1563
1657
|
if not re.match(r"^[a-zA-Z0-9._-]+$", session_id):
|
|
1564
1658
|
return JSONResponse(status_code=400, content={"error": "Invalid session ID"})
|
|
1565
|
-
|
|
1566
|
-
# Find project directory
|
|
1567
1659
|
target = _find_session_dir(session_id)
|
|
1568
|
-
|
|
1569
1660
|
if target is None:
|
|
1570
1661
|
return JSONResponse(status_code=404, content={"error": "Session not found"})
|
|
1571
1662
|
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
cmd_args = ["quick", req.message]
|
|
1575
|
-
else:
|
|
1576
|
-
cmd_args = ["start", "--provider", "claude", str(target / "PRD.md")]
|
|
1577
|
-
|
|
1578
|
-
rc, output = await asyncio.get_running_loop().run_in_executor(
|
|
1579
|
-
None, lambda: _run_loki_cmd(cmd_args, cwd=str(target), timeout=300)
|
|
1580
|
-
)
|
|
1663
|
+
task = ChatTask()
|
|
1664
|
+
_chat_tasks[task.id] = task
|
|
1581
1665
|
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
["
|
|
1588
|
-
|
|
1666
|
+
async def run_chat() -> None:
|
|
1667
|
+
loop = asyncio.get_running_loop()
|
|
1668
|
+
if req.mode == "quick":
|
|
1669
|
+
cmd_args = ["quick", req.message]
|
|
1670
|
+
else:
|
|
1671
|
+
cmd_args = ["start", "--provider", "claude", str(target / "PRD.md")]
|
|
1672
|
+
rc, output = await loop.run_in_executor(
|
|
1673
|
+
None, lambda: _run_loki_cmd(cmd_args, cwd=str(target), timeout=300)
|
|
1589
1674
|
)
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1675
|
+
task.output_lines = output.splitlines()
|
|
1676
|
+
task.returncode = rc
|
|
1677
|
+
# Detect changed files
|
|
1678
|
+
try:
|
|
1679
|
+
import subprocess as _sp
|
|
1680
|
+
result = _sp.run(
|
|
1681
|
+
["git", "diff", "--name-only", "HEAD~1"],
|
|
1682
|
+
cwd=str(target), capture_output=True, text=True, timeout=10
|
|
1683
|
+
)
|
|
1684
|
+
if result.returncode == 0:
|
|
1685
|
+
task.files_changed = [f for f in result.stdout.strip().splitlines() if f]
|
|
1686
|
+
except Exception:
|
|
1687
|
+
pass
|
|
1688
|
+
task.complete = True
|
|
1689
|
+
|
|
1690
|
+
asyncio.create_task(run_chat())
|
|
1594
1691
|
|
|
1595
1692
|
return JSONResponse(content={
|
|
1596
|
-
"
|
|
1597
|
-
"
|
|
1598
|
-
|
|
1693
|
+
"task_id": task.id,
|
|
1694
|
+
"status": "running",
|
|
1695
|
+
})
|
|
1696
|
+
|
|
1697
|
+
|
|
1698
|
+
@app.get("/api/sessions/{session_id}/chat/{task_id}")
|
|
1699
|
+
async def get_chat_status(session_id: str, task_id: str) -> JSONResponse:
|
|
1700
|
+
"""Poll chat task status and get partial output."""
|
|
1701
|
+
task = _chat_tasks.get(task_id)
|
|
1702
|
+
if task is None:
|
|
1703
|
+
return JSONResponse(status_code=404, content={"error": "Task not found"})
|
|
1704
|
+
return JSONResponse(content={
|
|
1705
|
+
"task_id": task.id,
|
|
1706
|
+
"status": "complete" if task.complete else "running",
|
|
1707
|
+
"output_lines": task.output_lines,
|
|
1708
|
+
"returncode": task.returncode,
|
|
1709
|
+
"files_changed": task.files_changed,
|
|
1710
|
+
"complete": task.complete,
|
|
1599
1711
|
})
|
|
1600
1712
|
|
|
1601
1713
|
|
|
@@ -1659,6 +1771,160 @@ async def export_session(session_id: str) -> JSONResponse:
|
|
|
1659
1771
|
return JSONResponse(content={"output": output, "returncode": rc})
|
|
1660
1772
|
|
|
1661
1773
|
|
|
1774
|
+
# ---------------------------------------------------------------------------
|
|
1775
|
+
# Secrets management endpoints
|
|
1776
|
+
# ---------------------------------------------------------------------------
|
|
1777
|
+
|
|
1778
|
+
|
|
1779
|
+
@app.get("/api/secrets")
|
|
1780
|
+
async def get_secrets() -> JSONResponse:
|
|
1781
|
+
"""List secret keys (values masked)."""
|
|
1782
|
+
secrets = _load_secrets()
|
|
1783
|
+
masked = {k: "***" for k in secrets}
|
|
1784
|
+
return JSONResponse(content=masked)
|
|
1785
|
+
|
|
1786
|
+
|
|
1787
|
+
@app.post("/api/secrets")
|
|
1788
|
+
async def set_secret(req: SecretRequest) -> JSONResponse:
|
|
1789
|
+
"""Set or update a secret."""
|
|
1790
|
+
if not re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', req.key):
|
|
1791
|
+
return JSONResponse(status_code=400, content={"error": "Invalid key. Use ENV_VAR style names."})
|
|
1792
|
+
secrets = _load_secrets()
|
|
1793
|
+
secrets[req.key] = req.value
|
|
1794
|
+
_save_secrets(secrets)
|
|
1795
|
+
return JSONResponse(content={"set": True, "key": req.key})
|
|
1796
|
+
|
|
1797
|
+
|
|
1798
|
+
@app.delete("/api/secrets/{key}")
|
|
1799
|
+
async def delete_secret(key: str) -> JSONResponse:
|
|
1800
|
+
"""Delete a secret."""
|
|
1801
|
+
secrets = _load_secrets()
|
|
1802
|
+
if key not in secrets:
|
|
1803
|
+
return JSONResponse(status_code=404, content={"error": "Secret not found"})
|
|
1804
|
+
del secrets[key]
|
|
1805
|
+
_save_secrets(secrets)
|
|
1806
|
+
return JSONResponse(content={"deleted": True, "key": key})
|
|
1807
|
+
|
|
1808
|
+
|
|
1809
|
+
# ---------------------------------------------------------------------------
|
|
1810
|
+
# Preview info (smart project type detection)
|
|
1811
|
+
# ---------------------------------------------------------------------------
|
|
1812
|
+
|
|
1813
|
+
|
|
1814
|
+
@app.get("/api/sessions/{session_id}/preview-info")
|
|
1815
|
+
async def get_preview_info(session_id: str) -> JSONResponse:
|
|
1816
|
+
"""Detect project type and determine the best preview strategy."""
|
|
1817
|
+
if not re.match(r"^[a-zA-Z0-9._-]+$", session_id):
|
|
1818
|
+
return JSONResponse(status_code=400, content={"error": "Invalid session ID"})
|
|
1819
|
+
target = _find_session_dir(session_id)
|
|
1820
|
+
if target is None:
|
|
1821
|
+
return JSONResponse(status_code=404, content={"error": "Session not found"})
|
|
1822
|
+
|
|
1823
|
+
info: dict = {
|
|
1824
|
+
"type": "unknown",
|
|
1825
|
+
"preview_url": None,
|
|
1826
|
+
"entry_file": None,
|
|
1827
|
+
"dev_command": None,
|
|
1828
|
+
"port": None,
|
|
1829
|
+
"description": "No preview available",
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
# Detect project type from files
|
|
1833
|
+
files = {f.name for f in target.iterdir() if f.is_file()} if target.is_dir() else set()
|
|
1834
|
+
has_package_json = "package.json" in files
|
|
1835
|
+
has_index_html = (
|
|
1836
|
+
"index.html" in files
|
|
1837
|
+
or (target / "public" / "index.html").exists()
|
|
1838
|
+
or (target / "src" / "index.html").exists()
|
|
1839
|
+
)
|
|
1840
|
+
has_pyproject = "pyproject.toml" in files or "setup.py" in files or "requirements.txt" in files
|
|
1841
|
+
has_go_mod = "go.mod" in files
|
|
1842
|
+
has_cargo = "Cargo.toml" in files
|
|
1843
|
+
has_dockerfile = "Dockerfile" in files or "docker-compose.yml" in files
|
|
1844
|
+
|
|
1845
|
+
# Read package.json for more info
|
|
1846
|
+
pkg_scripts: dict = {}
|
|
1847
|
+
pkg_deps: dict = {}
|
|
1848
|
+
if has_package_json:
|
|
1849
|
+
try:
|
|
1850
|
+
pkg = json.loads((target / "package.json").read_text())
|
|
1851
|
+
pkg_scripts = pkg.get("scripts", {})
|
|
1852
|
+
pkg_deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})}
|
|
1853
|
+
except (json.JSONDecodeError, OSError):
|
|
1854
|
+
pass
|
|
1855
|
+
|
|
1856
|
+
# Determine project type and preview strategy
|
|
1857
|
+
is_react = "react" in pkg_deps or "next" in pkg_deps or "vite" in pkg_deps
|
|
1858
|
+
is_express = "express" in pkg_deps or "fastify" in pkg_deps or "koa" in pkg_deps or "hono" in pkg_deps
|
|
1859
|
+
is_flask = has_pyproject and any((target / f).exists() for f in ["app.py", "main.py", "server.py"])
|
|
1860
|
+
is_fastapi = has_pyproject and any(
|
|
1861
|
+
"fastapi" in (target / f).read_text(errors="replace")
|
|
1862
|
+
for f in ["app.py", "main.py", "server.py"]
|
|
1863
|
+
if (target / f).exists()
|
|
1864
|
+
)
|
|
1865
|
+
|
|
1866
|
+
if is_react or (has_package_json and has_index_html):
|
|
1867
|
+
info["type"] = "web-app"
|
|
1868
|
+
info["entry_file"] = "index.html"
|
|
1869
|
+
info["preview_url"] = f"/api/sessions/{session_id}/preview/index.html"
|
|
1870
|
+
info["dev_command"] = pkg_scripts.get("dev") or pkg_scripts.get("start")
|
|
1871
|
+
info["description"] = "Web application -- serves HTML/CSS/JS"
|
|
1872
|
+
elif is_express or (has_package_json and ("start" in pkg_scripts or "dev" in pkg_scripts) and not has_index_html):
|
|
1873
|
+
# API/server project
|
|
1874
|
+
port = 3000 # default
|
|
1875
|
+
# Try to detect port from scripts
|
|
1876
|
+
start_script = pkg_scripts.get("start", "") + pkg_scripts.get("dev", "")
|
|
1877
|
+
port_match = re.search(r"(?:PORT|port)[=: ]*(\d+)", start_script)
|
|
1878
|
+
if port_match:
|
|
1879
|
+
port = int(port_match.group(1))
|
|
1880
|
+
info["type"] = "api"
|
|
1881
|
+
info["port"] = port
|
|
1882
|
+
info["dev_command"] = pkg_scripts.get("dev") or pkg_scripts.get("start")
|
|
1883
|
+
info["description"] = f"API server -- runs on port {port}"
|
|
1884
|
+
# Check for swagger/openapi
|
|
1885
|
+
for swagger_path in ["swagger.json", "openapi.json", "docs", "api-docs"]:
|
|
1886
|
+
if (target / swagger_path).exists():
|
|
1887
|
+
info["preview_url"] = f"/api/sessions/{session_id}/preview/{swagger_path}"
|
|
1888
|
+
break
|
|
1889
|
+
elif is_fastapi or is_flask:
|
|
1890
|
+
info["type"] = "python-api"
|
|
1891
|
+
info["port"] = 8000
|
|
1892
|
+
info["dev_command"] = "uvicorn app:app --reload" if is_fastapi else "flask run"
|
|
1893
|
+
info["description"] = "Python API server"
|
|
1894
|
+
elif has_index_html:
|
|
1895
|
+
info["type"] = "static-site"
|
|
1896
|
+
info["entry_file"] = "index.html"
|
|
1897
|
+
info["preview_url"] = f"/api/sessions/{session_id}/preview/index.html"
|
|
1898
|
+
info["description"] = "Static site -- serves HTML directly"
|
|
1899
|
+
elif has_package_json and "test" in pkg_scripts:
|
|
1900
|
+
info["type"] = "library"
|
|
1901
|
+
info["dev_command"] = pkg_scripts.get("test")
|
|
1902
|
+
info["description"] = "Library/package -- run tests to verify"
|
|
1903
|
+
elif has_go_mod:
|
|
1904
|
+
info["type"] = "go-app"
|
|
1905
|
+
info["dev_command"] = "go run ."
|
|
1906
|
+
info["description"] = "Go application"
|
|
1907
|
+
elif has_cargo:
|
|
1908
|
+
info["type"] = "rust-app"
|
|
1909
|
+
info["dev_command"] = "cargo run"
|
|
1910
|
+
info["description"] = "Rust application"
|
|
1911
|
+
elif has_dockerfile:
|
|
1912
|
+
info["type"] = "containerized"
|
|
1913
|
+
info["dev_command"] = "docker compose up"
|
|
1914
|
+
info["description"] = "Containerized application"
|
|
1915
|
+
else:
|
|
1916
|
+
# Check for any README or docs
|
|
1917
|
+
for doc_file in ["README.md", "readme.md", "README.txt"]:
|
|
1918
|
+
if (target / doc_file).exists():
|
|
1919
|
+
info["type"] = "project"
|
|
1920
|
+
info["entry_file"] = doc_file
|
|
1921
|
+
info["preview_url"] = f"/api/sessions/{session_id}/preview/{doc_file}"
|
|
1922
|
+
info["description"] = "Project -- showing README"
|
|
1923
|
+
break
|
|
1924
|
+
|
|
1925
|
+
return JSONResponse(content=info)
|
|
1926
|
+
|
|
1927
|
+
|
|
1662
1928
|
# ---------------------------------------------------------------------------
|
|
1663
1929
|
# Health check
|
|
1664
1930
|
# ---------------------------------------------------------------------------
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{r as p,j as t}from"./index-DvhjaUe4.js";const m={primary:"bg-[#553DE9] text-white hover:bg-[#4432c4] shadow-button rounded-btn",secondary:"border border-[#553DE9] text-[#553DE9] hover:bg-[#E8E4FD] bg-transparent rounded-btn",ghost:"text-[#36342E] hover:bg-[#F8F4F0] rounded-btn",danger:"bg-[#C45B5B]/10 text-[#C45B5B] border border-[#C45B5B]/20 hover:bg-[#C45B5B]/20 rounded-btn"},b={sm:"px-3 py-1.5 text-xs",md:"px-4 py-2 text-sm",lg:"px-6 py-3 text-base"},u={sm:14,md:16,lg:18};function h({size:e}){return t.jsxs("svg",{className:"animate-spin",width:e,height:e,viewBox:"0 0 24 24",fill:"none",children:[t.jsx("circle",{className:"opacity-25",cx:"12",cy:"12",r:"10",stroke:"currentColor",strokeWidth:"4"}),t.jsx("path",{className:"opacity-75",fill:"currentColor",d:"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"})]})}const B=p.forwardRef(({variant:e="primary",size:o="md",icon:n,iconRight:i,loading:r=!1,disabled:a,className:c="",children:l,...x},d)=>{const s=u[o];return t.jsxs("button",{ref:d,disabled:a||r,className:["inline-flex items-center justify-center gap-2 font-medium transition-colors",m[e],b[o],(a||r)&&"opacity-60 cursor-not-allowed",c].filter(Boolean).join(" "),...x,children:[r?t.jsx(h,{size:s}):n?t.jsx(n,{size:s}):null,l,i&&!r&&t.jsx(i,{size:s})]})});B.displayName="Button";export{B};
|