Полное перестроение контейнера с исправлением проблем прав доступа и добавлением прямого чтения property.json из API
Browse files- Dockerfile +28 -4
- app.py +146 -60
Dockerfile
CHANGED
@@ -38,6 +38,9 @@ RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
|
|
38 |
apt-get install -y nodejs && \
|
39 |
npm install -g pnpm
|
40 |
|
|
|
|
|
|
|
41 |
# Устанавливаем рабочую директорию
|
42 |
WORKDIR /app
|
43 |
|
@@ -54,12 +57,24 @@ RUN go install github.com/go-task/task/v3/cmd/task@latest
|
|
54 |
RUN mkdir -p /app/server/bin && \
|
55 |
mkdir -p /app/agents && \
|
56 |
mkdir -p /tmp/ten_agent && \
|
57 |
-
mkdir -p /app/.pnpm-store
|
|
|
|
|
|
|
58 |
|
59 |
# Копируем .env файл
|
60 |
COPY .env /app/.env
|
|
|
|
|
|
|
|
|
|
|
61 |
|
62 |
-
#
|
|
|
|
|
|
|
|
|
63 |
RUN echo 'module.exports = { \
|
64 |
async rewrites() { \
|
65 |
return [ \
|
@@ -158,11 +173,20 @@ RUN echo '{\n\
|
|
158 |
}' > /app/agents/chat_agent.json
|
159 |
|
160 |
# Устанавливаем правильные права доступа для всех файлов
|
161 |
-
RUN chmod -R 777 /app
|
|
|
|
|
|
|
|
|
|
|
162 |
|
163 |
# Настраиваем стартовый скрипт
|
164 |
COPY app.py /app/app.py
|
165 |
-
RUN chmod +x /app/app.py
|
|
|
|
|
|
|
|
|
166 |
|
167 |
# Открываем порты
|
168 |
EXPOSE 7860 8080 3000
|
|
|
38 |
apt-get install -y nodejs && \
|
39 |
npm install -g pnpm
|
40 |
|
41 |
+
# Создаем пользователя tenuser с правильными правами
|
42 |
+
RUN useradd -m -u 1000 -s /bin/bash tenuser
|
43 |
+
|
44 |
# Устанавливаем рабочую директорию
|
45 |
WORKDIR /app
|
46 |
|
|
|
57 |
RUN mkdir -p /app/server/bin && \
|
58 |
mkdir -p /app/agents && \
|
59 |
mkdir -p /tmp/ten_agent && \
|
60 |
+
mkdir -p /app/.pnpm-store && \
|
61 |
+
mkdir -p /app/backup && \
|
62 |
+
chown -R tenuser:tenuser /app && \
|
63 |
+
chown -R tenuser:tenuser /tmp/ten_agent
|
64 |
|
65 |
# Копируем .env файл
|
66 |
COPY .env /app/.env
|
67 |
+
RUN chown tenuser:tenuser /app/.env
|
68 |
+
|
69 |
+
# Патчим файлы для поддержки чтения графов
|
70 |
+
# 1. Патчим метод чтения графов в API сервере для прямого чтения из файла property.json
|
71 |
+
RUN echo 'package internal\n\nimport (\n\t"encoding/json"\n\t"fmt"\n\t"io/ioutil"\n\t"os"\n\t"path/filepath"\n)\n\n// GraphInfo представляет информацию о графе\ntype GraphInfo struct {\n\tName string `json:"name"`\n\tDescription string `json:"description"`\n\tFile string `json:"file"`\n}\n\n// PropertyJson структура файла property.json\ntype PropertyJson struct {\n\tTen map[string]interface{} `json:"_ten"`\n\tName string `json:"name"`\n\tVersion string `json:"version"`\n\tExtensions []string `json:"extensions"`\n\tDescription string `json:"description"`\n\tGraphs []GraphInfo `json:"graphs"`\n}\n\n// ReadGraphsFromPropertyJson читает список графов из property.json\nfunc ReadGraphsFromPropertyJson() ([]GraphInfo, error) {\n\tpropertyPath := filepath.Join("/app/agents", "property.json")\n\n\tdata, err := ioutil.ReadFile(propertyPath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf("ошибка чтения property.json: %v", err)\n\t}\n\n\tvar property PropertyJson\n\terr = json.Unmarshal(data, &property)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf("ошибка разбора property.json: %v", err)\n\t}\n\n\treturn property.Graphs, nil\n}' > /app/server/internal/property_parser.go
|
72 |
|
73 |
+
# 2. Патчим обработчик для чтения графов из property.json
|
74 |
+
RUN sed -i 's/func (s \*HttpServer) handleGraphs/func oldHandleGraphs/g' /app/server/internal/http_server.go && \
|
75 |
+
echo 'func (s *HttpServer) handleGraphs(c *gin.Context) {\n\tgraphs, err := ReadGraphsFromPropertyJson()\n\tif err != nil {\n\t\ts.logger.Error(fmt.Sprintf("Failed to read graphs: %v", err))\n\t\tc.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})\n\t\treturn\n\t}\n\tc.JSON(http.StatusOK, graphs)\n}' >> /app/server/internal/http_server.go
|
76 |
+
|
77 |
+
# Патчим файл конфигурации Next.js для перенаправления запросов
|
78 |
RUN echo 'module.exports = { \
|
79 |
async rewrites() { \
|
80 |
return [ \
|
|
|
173 |
}' > /app/agents/chat_agent.json
|
174 |
|
175 |
# Устанавливаем правильные права доступа для всех файлов
|
176 |
+
RUN chmod -R 777 /app && \
|
177 |
+
chown -R tenuser:tenuser /app/agents && \
|
178 |
+
chown -R tenuser:tenuser /app/server && \
|
179 |
+
find /app -type d -exec chmod 777 {} \; && \
|
180 |
+
find /app -type f -exec chmod 666 {} \; && \
|
181 |
+
chmod +x /app/server/bin/api
|
182 |
|
183 |
# Настраиваем стартовый скрипт
|
184 |
COPY app.py /app/app.py
|
185 |
+
RUN chmod +x /app/app.py && \
|
186 |
+
chown tenuser:tenuser /app/app.py
|
187 |
+
|
188 |
+
# Переключаемся на пользователя tenuser
|
189 |
+
USER tenuser
|
190 |
|
191 |
# Открываем порты
|
192 |
EXPOSE 7860 8080 3000
|
app.py
CHANGED
@@ -9,6 +9,9 @@ import signal
|
|
9 |
import threading
|
10 |
import shutil
|
11 |
import logging
|
|
|
|
|
|
|
12 |
|
13 |
# Настройка логирования
|
14 |
logging.basicConfig(level=logging.INFO,
|
@@ -17,12 +20,47 @@ logging.basicConfig(level=logging.INFO,
|
|
17 |
|
18 |
logger = logging.getLogger('ten-agent')
|
19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
20 |
def check_and_create_property_json():
|
21 |
"""Проверяет наличие property.json и создает его при необходимости"""
|
22 |
-
|
23 |
-
|
24 |
-
if not property_path.exists():
|
25 |
-
logger.warning(f"{property_path} не найден, создаем файл...")
|
26 |
|
27 |
property_data = {
|
28 |
"_ten": {}, # Важное поле для TEN формата
|
@@ -45,22 +83,85 @@ def check_and_create_property_json():
|
|
45 |
}
|
46 |
|
47 |
# Проверяем и создаем директории
|
48 |
-
|
49 |
|
50 |
-
#
|
51 |
-
with
|
52 |
-
json.dump(property_data,
|
53 |
-
|
54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
55 |
|
56 |
def check_files():
|
57 |
"""Проверяет и выводит информацию о важных файлах"""
|
58 |
files_to_check = [
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
]
|
65 |
|
66 |
logger.info("=== Проверка критических файлов ===")
|
@@ -72,7 +173,7 @@ def check_files():
|
|
72 |
logger.info(f"✅ {file_path} (размер: {size} байт)")
|
73 |
|
74 |
# Если это JSON файл, выводим его содержимое
|
75 |
-
if file_path.endswith('.json'):
|
76 |
try:
|
77 |
with open(file_path, 'r') as f:
|
78 |
content = json.load(f)
|
@@ -85,43 +186,15 @@ def check_files():
|
|
85 |
logger.error(f"❌ {file_path} (файл не найден)")
|
86 |
|
87 |
logger.info("=== Проверка структуры директорий ===")
|
88 |
-
logger.info("Содержимое
|
89 |
-
subprocess.run(["ls", "-la",
|
90 |
|
91 |
logger.info("Проверка прав доступа:")
|
92 |
-
subprocess.run(["stat",
|
93 |
-
subprocess.run(["stat",
|
94 |
-
|
95 |
-
def setup_mock_designer():
|
96 |
-
"""Настраивает мок-эндпоинты для дизайнера"""
|
97 |
-
# Создаем файл с мок-ответом для запросов
|
98 |
-
mock_dir = Path("/app/mock_responses")
|
99 |
-
mock_dir.mkdir(exist_ok=True)
|
100 |
-
|
101 |
-
reload_response = {
|
102 |
-
"success": True,
|
103 |
-
"packages": [
|
104 |
-
{
|
105 |
-
"name": "default",
|
106 |
-
"description": "Default package",
|
107 |
-
"graphs": [
|
108 |
-
{"name": "Voice Agent", "description": "Voice Agent with OpenAI", "file": "voice_agent.json"},
|
109 |
-
{"name": "Chat Agent", "description": "Simple Chat Agent", "file": "chat_agent.json"}
|
110 |
-
]
|
111 |
-
}
|
112 |
-
]
|
113 |
-
}
|
114 |
-
|
115 |
-
with open(mock_dir / "reload_response.json", "w") as f:
|
116 |
-
json.dump(reload_response, f, indent=2)
|
117 |
-
|
118 |
-
logger.info("Мок-файлы для дизайнера созданы")
|
119 |
|
120 |
def test_api():
|
121 |
"""Делает запрос к API для получения списка графов"""
|
122 |
-
import urllib.request
|
123 |
-
import urllib.error
|
124 |
-
|
125 |
logger.info("=== Тестирование API ===")
|
126 |
try:
|
127 |
# Даем серверу время запуститься
|
@@ -135,6 +208,18 @@ def test_api():
|
|
135 |
json_data = json.loads(data)
|
136 |
if isinstance(json_data, list) and len(json_data) > 0:
|
137 |
logger.info(f"API вернул {len(json_data)} графов")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
138 |
else:
|
139 |
logger.warning("API вернул пустой список графов")
|
140 |
except json.JSONDecodeError:
|
@@ -147,31 +232,31 @@ def test_api():
|
|
147 |
def main():
|
148 |
processes = []
|
149 |
try:
|
150 |
-
# Пути к исполняемым файлам
|
151 |
-
api_binary = Path("/app/server/bin/api")
|
152 |
-
playground_dir = Path("/app/playground")
|
153 |
-
|
154 |
# Проверяем существование файлов
|
155 |
-
if not
|
156 |
-
logger.error(f"API binary не найден: {
|
157 |
return 1
|
158 |
|
159 |
-
if not
|
160 |
-
logger.error(f"Playground директория не найдена: {
|
161 |
return 1
|
162 |
|
|
|
|
|
|
|
|
|
163 |
# Проверяем и создаем property.json
|
164 |
check_and_create_property_json()
|
165 |
|
166 |
-
#
|
167 |
-
|
168 |
|
169 |
# Проверка файлов перед запуском
|
170 |
check_files()
|
171 |
|
172 |
# Запускаем API сервер
|
173 |
logger.info("Запуск TEN-Agent API сервера на порту 8080...")
|
174 |
-
api_process = subprocess.Popen([str(
|
175 |
processes.append(api_process)
|
176 |
|
177 |
# Тестируем API
|
@@ -191,9 +276,10 @@ def main():
|
|
191 |
os.environ["NEXT_PUBLIC_API_BASE_URL"] = "/api/agents"
|
192 |
os.environ["NEXT_PUBLIC_DESIGNER_API_URL"] = "http://localhost:8080"
|
193 |
|
|
|
194 |
playground_process = subprocess.Popen(
|
195 |
["pnpm", "dev"],
|
196 |
-
cwd=str(
|
197 |
env=os.environ
|
198 |
)
|
199 |
processes.append(playground_process)
|
|
|
9 |
import threading
|
10 |
import shutil
|
11 |
import logging
|
12 |
+
import urllib.request
|
13 |
+
import urllib.error
|
14 |
+
import tempfile
|
15 |
|
16 |
# Настройка логирования
|
17 |
logging.basicConfig(level=logging.INFO,
|
|
|
20 |
|
21 |
logger = logging.getLogger('ten-agent')
|
22 |
|
23 |
+
# Глобальные пути
|
24 |
+
AGENTS_DIR = Path("/app/agents")
|
25 |
+
PROPERTY_JSON = AGENTS_DIR / "property.json"
|
26 |
+
MANIFEST_JSON = AGENTS_DIR / "manifest.json"
|
27 |
+
VOICE_AGENT_JSON = AGENTS_DIR / "voice_agent.json"
|
28 |
+
CHAT_AGENT_JSON = AGENTS_DIR / "chat_agent.json"
|
29 |
+
API_BINARY = Path("/app/server/bin/api")
|
30 |
+
PLAYGROUND_DIR = Path("/app/playground")
|
31 |
+
BACKUP_DIR = Path("/app/backup")
|
32 |
+
|
33 |
+
def ensure_directory_permissions(directory_path):
|
34 |
+
"""Обеспечиваем правильные разрешения для директории"""
|
35 |
+
directory = Path(directory_path)
|
36 |
+
if not directory.exists():
|
37 |
+
logger.info(f"Создание директории {directory}")
|
38 |
+
directory.mkdir(parents=True, exist_ok=True)
|
39 |
+
|
40 |
+
# Устанавливаем полные права
|
41 |
+
subprocess.run(["chmod", "-R", "777", str(directory)])
|
42 |
+
logger.info(f"Права доступа для {directory} установлены")
|
43 |
+
|
44 |
+
def backup_file(filepath):
|
45 |
+
"""Создает резервную копию файла"""
|
46 |
+
src_path = Path(filepath)
|
47 |
+
if not src_path.exists():
|
48 |
+
logger.warning(f"Невозможно создать резервную копию: {filepath} не существует")
|
49 |
+
return
|
50 |
+
|
51 |
+
BACKUP_DIR.mkdir(parents=True, exist_ok=True)
|
52 |
+
dest_path = BACKUP_DIR / f"{src_path.name}.bak"
|
53 |
+
|
54 |
+
try:
|
55 |
+
shutil.copy2(src_path, dest_path)
|
56 |
+
logger.info(f"Резервная копия создана: {dest_path}")
|
57 |
+
except Exception as e:
|
58 |
+
logger.error(f"Ошибка при создании резервной копии {filepath}: {e}")
|
59 |
+
|
60 |
def check_and_create_property_json():
|
61 |
"""Проверяет наличие property.json и создает его при необходимости"""
|
62 |
+
if not PROPERTY_JSON.exists():
|
63 |
+
logger.warning(f"{PROPERTY_JSON} не найден, создаем файл...")
|
|
|
|
|
64 |
|
65 |
property_data = {
|
66 |
"_ten": {}, # Важное поле для TEN формата
|
|
|
83 |
}
|
84 |
|
85 |
# Проверяем и создаем директории
|
86 |
+
PROPERTY_JSON.parent.mkdir(parents=True, exist_ok=True)
|
87 |
|
88 |
+
# Создаем временный файл и затем перемещаем его
|
89 |
+
with tempfile.NamedTemporaryFile(mode='w', delete=False) as temp_file:
|
90 |
+
json.dump(property_data, temp_file, indent=2)
|
91 |
+
temp_path = temp_file.name
|
92 |
+
|
93 |
+
# Копируем временный файл в целевой
|
94 |
+
try:
|
95 |
+
shutil.copy2(temp_path, PROPERTY_JSON)
|
96 |
+
os.chmod(PROPERTY_JSON, 0o666) # Устанавливаем права доступа rw-rw-rw-
|
97 |
+
logger.info(f"Файл {PROPERTY_JSON} создан успешно")
|
98 |
+
except Exception as e:
|
99 |
+
logger.error(f"Ошибка при создании {PROPERTY_JSON}: {e}")
|
100 |
+
finally:
|
101 |
+
os.unlink(temp_path) # Удаляем временный файл
|
102 |
+
|
103 |
+
def check_and_create_agent_files():
|
104 |
+
"""Проверяет наличие всех необходимых файлов агентов и создает их при необходимости"""
|
105 |
+
|
106 |
+
# Создаем manifest.json если он не существует
|
107 |
+
if not MANIFEST_JSON.exists():
|
108 |
+
manifest_data = {
|
109 |
+
"name": "default",
|
110 |
+
"agents": [
|
111 |
+
{
|
112 |
+
"name": "voice_agent",
|
113 |
+
"description": "A simple voice agent"
|
114 |
+
},
|
115 |
+
{
|
116 |
+
"name": "chat_agent",
|
117 |
+
"description": "A text chat agent"
|
118 |
+
}
|
119 |
+
]
|
120 |
+
}
|
121 |
+
|
122 |
+
with open(MANIFEST_JSON, 'w') as f:
|
123 |
+
json.dump(manifest_data, f, indent=2)
|
124 |
+
os.chmod(MANIFEST_JSON, 0o666)
|
125 |
+
logger.info(f"Файл {MANIFEST_JSON} создан")
|
126 |
+
|
127 |
+
# Создаем voice_agent.json если он не существует
|
128 |
+
if not VOICE_AGENT_JSON.exists():
|
129 |
+
voice_agent_data = {
|
130 |
+
"nodes": [],
|
131 |
+
"edges": [],
|
132 |
+
"groups": [],
|
133 |
+
"templates": [],
|
134 |
+
"root": None
|
135 |
+
}
|
136 |
+
|
137 |
+
with open(VOICE_AGENT_JSON, 'w') as f:
|
138 |
+
json.dump(voice_agent_data, f, indent=2)
|
139 |
+
os.chmod(VOICE_AGENT_JSON, 0o666)
|
140 |
+
logger.info(f"Файл {VOICE_AGENT_JSON} создан")
|
141 |
+
|
142 |
+
# Создаем chat_agent.json если он не существует
|
143 |
+
if not CHAT_AGENT_JSON.exists():
|
144 |
+
chat_agent_data = {
|
145 |
+
"nodes": [],
|
146 |
+
"edges": [],
|
147 |
+
"groups": [],
|
148 |
+
"templates": [],
|
149 |
+
"root": None
|
150 |
+
}
|
151 |
+
|
152 |
+
with open(CHAT_AGENT_JSON, 'w') as f:
|
153 |
+
json.dump(chat_agent_data, f, indent=2)
|
154 |
+
os.chmod(CHAT_AGENT_JSON, 0o666)
|
155 |
+
logger.info(f"Файл {CHAT_AGENT_JSON} создан")
|
156 |
|
157 |
def check_files():
|
158 |
"""Проверяет и выводит информацию о важных файлах"""
|
159 |
files_to_check = [
|
160 |
+
PROPERTY_JSON,
|
161 |
+
MANIFEST_JSON,
|
162 |
+
VOICE_AGENT_JSON,
|
163 |
+
CHAT_AGENT_JSON,
|
164 |
+
API_BINARY
|
165 |
]
|
166 |
|
167 |
logger.info("=== Проверка критических файлов ===")
|
|
|
173 |
logger.info(f"✅ {file_path} (размер: {size} байт)")
|
174 |
|
175 |
# Если это JSON файл, выводим его содержимое
|
176 |
+
if str(file_path).endswith('.json'):
|
177 |
try:
|
178 |
with open(file_path, 'r') as f:
|
179 |
content = json.load(f)
|
|
|
186 |
logger.error(f"❌ {file_path} (файл не найден)")
|
187 |
|
188 |
logger.info("=== Проверка структуры директорий ===")
|
189 |
+
logger.info(f"Содержимое {AGENTS_DIR}:")
|
190 |
+
subprocess.run(["ls", "-la", str(AGENTS_DIR)])
|
191 |
|
192 |
logger.info("Проверка прав доступа:")
|
193 |
+
subprocess.run(["stat", str(AGENTS_DIR)])
|
194 |
+
subprocess.run(["stat", str(PROPERTY_JSON)])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
195 |
|
196 |
def test_api():
|
197 |
"""Делает запрос к API для получения списка графов"""
|
|
|
|
|
|
|
198 |
logger.info("=== Тестирование API ===")
|
199 |
try:
|
200 |
# Даем серверу время запуститься
|
|
|
208 |
json_data = json.loads(data)
|
209 |
if isinstance(json_data, list) and len(json_data) > 0:
|
210 |
logger.info(f"API вернул {len(json_data)} графов")
|
211 |
+
# Если API вернул пустой список, исправляем это
|
212 |
+
if len(json_data) == 0:
|
213 |
+
logger.warning("API вернул пустой список графов, исправляем property.json")
|
214 |
+
backup_file(PROPERTY_JSON)
|
215 |
+
check_and_create_property_json()
|
216 |
+
check_and_create_agent_files()
|
217 |
+
ensure_directory_permissions(AGENTS_DIR)
|
218 |
+
# Перезапускаем API сервер
|
219 |
+
logger.info("Перезапускаем API сервер...")
|
220 |
+
subprocess.run(["pkill", "-f", str(API_BINARY)])
|
221 |
+
time.sleep(1)
|
222 |
+
subprocess.Popen([str(API_BINARY)])
|
223 |
else:
|
224 |
logger.warning("API вернул пустой список графов")
|
225 |
except json.JSONDecodeError:
|
|
|
232 |
def main():
|
233 |
processes = []
|
234 |
try:
|
|
|
|
|
|
|
|
|
235 |
# Проверяем существование файлов
|
236 |
+
if not API_BINARY.exists():
|
237 |
+
logger.error(f"API binary не найден: {API_BINARY}")
|
238 |
return 1
|
239 |
|
240 |
+
if not PLAYGROUND_DIR.exists():
|
241 |
+
logger.error(f"Playground директория не найдена: {PLAYGROUND_DIR}")
|
242 |
return 1
|
243 |
|
244 |
+
# Создаем директории и устанавливаем права
|
245 |
+
ensure_directory_permissions(AGENTS_DIR)
|
246 |
+
ensure_directory_permissions(BACKUP_DIR)
|
247 |
+
|
248 |
# Проверяем и создаем property.json
|
249 |
check_and_create_property_json()
|
250 |
|
251 |
+
# Проверяем и создаем файлы агентов
|
252 |
+
check_and_create_agent_files()
|
253 |
|
254 |
# Проверка файлов перед запуском
|
255 |
check_files()
|
256 |
|
257 |
# Запускаем API сервер
|
258 |
logger.info("Запуск TEN-Agent API сервера на порту 8080...")
|
259 |
+
api_process = subprocess.Popen([str(API_BINARY)])
|
260 |
processes.append(api_process)
|
261 |
|
262 |
# Тестируем API
|
|
|
276 |
os.environ["NEXT_PUBLIC_API_BASE_URL"] = "/api/agents"
|
277 |
os.environ["NEXT_PUBLIC_DESIGNER_API_URL"] = "http://localhost:8080"
|
278 |
|
279 |
+
# Запускаем Playground UI
|
280 |
playground_process = subprocess.Popen(
|
281 |
["pnpm", "dev"],
|
282 |
+
cwd=str(PLAYGROUND_DIR),
|
283 |
env=os.environ
|
284 |
)
|
285 |
processes.append(playground_process)
|