@weppy/roblox-mcp 2.0.9 → 2.0.10
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/.claude-plugin/marketplace.json +2 -2
- package/CHANGELOG.md +9 -0
- package/README.md +3 -1
- package/docs/assets/screenshots/dashboard/dashboard_playtest.png +0 -0
- package/docs/assets/screenshots/plugin/sync/sync-overview.png +0 -0
- package/docs/en/installation/roblox-plugin.md +4 -4
- package/docs/en/pro-upgrade.md +2 -2
- package/docs/en/sync/overview.md +2 -2
- package/docs/es/README.md +3 -1
- package/docs/es/installation/roblox-plugin.md +4 -4
- package/docs/es/pro-upgrade.md +2 -2
- package/docs/es/sync/overview.md +2 -2
- package/docs/id/README.md +3 -1
- package/docs/id/installation/roblox-plugin.md +4 -4
- package/docs/id/pro-upgrade.md +2 -2
- package/docs/id/sync/overview.md +2 -2
- package/docs/ja/README.md +3 -1
- package/docs/ja/installation/roblox-plugin.md +4 -4
- package/docs/ja/pro-upgrade.md +2 -2
- package/docs/ja/sync/overview.md +2 -2
- package/docs/ko/README.md +3 -1
- package/docs/ko/installation/roblox-plugin.md +4 -4
- package/docs/ko/pro-upgrade.md +2 -2
- package/docs/ko/sync/overview.md +2 -2
- package/docs/pt-br/README.md +3 -1
- package/docs/pt-br/installation/roblox-plugin.md +4 -4
- package/docs/pt-br/pro-upgrade.md +2 -2
- package/docs/pt-br/sync/overview.md +2 -2
- package/package.json +1 -1
- package/plugins/weppy-roblox-mcp/.claude-plugin/plugin.json +1 -1
- package/plugins/weppy-roblox-mcp/dist/index.js +1 -1
- package/plugins/weppy-roblox-mcp/roblox-plugin/WeppyRobloxMCP.rbxm +0 -0
- package/docs/assets/screenshots/connection_popup.png +0 -0
- package/docs/assets/screenshots/sync.png +0 -0
- /package/docs/assets/screenshots/{connection_guide.png → plugin/connection/connection-guide.png} +0 -0
- /package/docs/assets/screenshots/{plugin_main.png → plugin/installation/main-screen.png} +0 -0
- /package/docs/assets/screenshots/{plugins_menu.png → plugin/installation/plugins-menu.png} +0 -0
- /package/docs/assets/screenshots/{settings.png → plugin/installation/settings-screen.png} +0 -0
- /package/docs/assets/screenshots/{weppy_plugin_toolbar.png → plugin/installation/toolbar-button.png} +0 -0
- /package/docs/assets/screenshots/{license/license-dashboard.png → plugin/license/dashboard-license-screen.png} +0 -0
- /package/docs/assets/screenshots/{license/license-plugin.png → plugin/license/plugin-license-screen.png} +0 -0
- /package/docs/assets/screenshots/{sync_conflict.png → plugin/sync/sync-conflict.png} +0 -0
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "Weppy Roblox MCP — MCP server that lets AI coding agents control a live Roblox Studio session with 21 tools, 140+ actions, bidirectional sync, automated playtest, and multi-place support",
|
|
9
|
-
"version": "2.0.
|
|
9
|
+
"version": "2.0.10"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
13
13
|
"name": "weppy-roblox-mcp",
|
|
14
14
|
"source": "./plugins/weppy-roblox-mcp",
|
|
15
15
|
"description": "Weppy Roblox MCP — MCP server that lets AI coding agents control a live Roblox Studio session with 21 tools, 140+ actions, bidirectional sync, automated playtest, and multi-place support",
|
|
16
|
-
"version": "2.0.
|
|
16
|
+
"version": "2.0.10",
|
|
17
17
|
"author": {
|
|
18
18
|
"name": "hope1026"
|
|
19
19
|
},
|
package/CHANGELOG.md
CHANGED
|
@@ -21,6 +21,15 @@ All notable changes to this project will be documented in this file.
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
|
|
24
|
+
|
|
25
|
+
## [2.0.10] - 2026-03-28
|
|
26
|
+
|
|
27
|
+
### Improved
|
|
28
|
+
|
|
29
|
+
- Improved sync logic with play mode detection, suppression, and post-play reconciliation.
|
|
30
|
+
- Enhanced plugin UI for sync collision resolution.
|
|
31
|
+
|
|
32
|
+
|
|
24
33
|
## [2.0.9] - 2026-03-27
|
|
25
34
|
|
|
26
35
|
### Improved
|
package/README.md
CHANGED
|
@@ -88,7 +88,7 @@ AI can directly handle scripts, instances, properties, terrain, lighting, assets
|
|
|
88
88
|
|
|
89
89
|
AI works from a synchronized local mirror, so multi-file updates stay consistent.
|
|
90
90
|
|
|
91
|
-

|
|
91
|
+

|
|
92
92
|
|
|
93
93
|
- Basic: one-way sync (Studio -> Local)
|
|
94
94
|
- Pro: bidirectional sync + per-type Direction/Apply Mode + history + multi-place
|
|
@@ -101,6 +101,8 @@ AI can control Roblox Studio playtests directly. It can start and stop Play (F5)
|
|
|
101
101
|
- "Write a test that verifies the SpawnLocation is above the ground and run it."
|
|
102
102
|
- "Validate that the script I just changed runs without errors in playtest."
|
|
103
103
|
|
|
104
|
+

|
|
105
|
+
|
|
104
106
|
### 4) WROX Dashboard: Monitor AI work in real time
|
|
105
107
|
|
|
106
108
|
The MCP server provides a web dashboard where you can check connection status, tool execution history, sync state, and game change logs in real time.
|
|
Binary file
|
|
Binary file
|
|
@@ -18,7 +18,7 @@ Note:
|
|
|
18
18
|
2. Click the **Plugins** tab in the top menu
|
|
19
19
|
3. Click the **Plugins Folder** button
|
|
20
20
|
|
|
21
|
-

|
|
22
22
|
|
|
23
23
|
4. **Copy** the `WeppyRobloxMCP.rbxm` file from the extracted folder into the opened Plugins folder
|
|
24
24
|
5. **Restart Roblox Studio**
|
|
@@ -27,7 +27,7 @@ Note:
|
|
|
27
27
|
|
|
28
28
|
After restarting, the **WROX** button will appear in the Plugins tab.
|
|
29
29
|
|
|
30
|
-

|
|
31
31
|
|
|
32
32
|
## 4. Connect to AI Agent
|
|
33
33
|
|
|
@@ -52,13 +52,13 @@ The MCP server must be installed. Complete the guide for your AI app first:
|
|
|
52
52
|
3. Click the **Connect** button in the plugin window
|
|
53
53
|
4. Once **"Connected"** status is displayed, you're ready
|
|
54
54
|
|
|
55
|
-

|
|
56
56
|
|
|
57
57
|
## 5. Settings (Optional)
|
|
58
58
|
|
|
59
59
|
Click the settings button in the top right of the plugin to change options.
|
|
60
60
|
|
|
61
|
-

|
|
61
|
+

|
|
62
62
|
|
|
63
63
|
- **Auto Connect**: Automatically connect to MCP server when Studio starts
|
|
64
64
|
- **Auto Reconnect**: Automatically attempt to reconnect when connection is lost
|
package/docs/en/pro-upgrade.md
CHANGED
|
@@ -48,7 +48,7 @@ You only need to activate the license once, either in the plugin or in the dashb
|
|
|
48
48
|
5. If the status does not update immediately, click **Refresh**.
|
|
49
49
|
6. When activation succeeds, the status changes from Basic to Pro and Pro features become available.
|
|
50
50
|
|
|
51
|
-

|
|
52
52
|
|
|
53
53
|
### Activate in the dashboard
|
|
54
54
|
|
|
@@ -58,7 +58,7 @@ You only need to activate the license once, either in the plugin or in the dashb
|
|
|
58
58
|
4. Click **Activate License** to activate the license.
|
|
59
59
|
5. If needed, use **Refresh License** to fetch the latest status.
|
|
60
60
|
|
|
61
|
-

|
|
62
62
|
|
|
63
63
|
### After activation
|
|
64
64
|
|
package/docs/en/sync/overview.md
CHANGED
|
@@ -12,7 +12,7 @@ Without Sync, AI only sees snippets pasted into chat. With Sync enabled, AI work
|
|
|
12
12
|
|
|
13
13
|
## How it works
|
|
14
14
|
|
|
15
|
-

|
|
15
|
+

|
|
16
16
|
|
|
17
17
|
1. Full Sync: initial mirror from Studio tree/instances to local files
|
|
18
18
|
2. Incremental Sync: continuous update of new changes
|
|
@@ -109,7 +109,7 @@ In Pro, Direction and Apply Mode can be controlled per type.
|
|
|
109
109
|
|
|
110
110
|
When changes are detected on both Studio and local sides during bidirectional sync, a conflict resolution dialog appears.
|
|
111
111
|
|
|
112
|
-
](../../assets/screenshots/plugin/sync/sync-conflict.png)
|
|
113
113
|
|
|
114
114
|
- **Studio Priority**: overwrite with Studio state as the source of truth
|
|
115
115
|
- **Local Priority**: apply local files to Studio
|
package/docs/es/README.md
CHANGED
|
@@ -79,7 +79,7 @@ No es solo generacion de codigo. Son **acciones ejecutables orientadas a producc
|
|
|
79
79
|
|
|
80
80
|
La IA trabaja sobre un espejo local sincronizado, asi que los cambios en multiples archivos se mantienen consistentes.
|
|
81
81
|
|
|
82
|
-

|
|
82
|
+

|
|
83
83
|
|
|
84
84
|
- Basic: sincronizacion unidireccional (Studio -> Local)
|
|
85
85
|
- Pro: sincronizacion bidireccional + Direction/Apply Mode por tipo + historial + multiplace
|
|
@@ -92,6 +92,8 @@ La IA puede controlar directamente el playtest de Studio. Puede iniciar y detene
|
|
|
92
92
|
- "Escribe y ejecuta una prueba para confirmar que el SpawnLocation esta sobre el suelo."
|
|
93
93
|
- "Valida con playtest que el script que acabo de cambiar funciona sin errores."
|
|
94
94
|
|
|
95
|
+

|
|
96
|
+
|
|
95
97
|
### 4) WROX Dashboard: monitorea el trabajo de la IA en tiempo real
|
|
96
98
|
|
|
97
99
|
El Dashboard web proporcionado por el servidor MCP permite consultar en tiempo real el estado de conexion, el historial de ejecucion de herramientas, el estado de sincronizacion y el historial de cambios del juego.
|
|
@@ -18,7 +18,7 @@ Nota:
|
|
|
18
18
|
2. Clic en la pestana **Plugins**
|
|
19
19
|
3. Clic en **Plugins Folder**
|
|
20
20
|
|
|
21
|
-

|
|
22
22
|
|
|
23
23
|
4. **Copia** `WeppyRobloxMCP.rbxm` a la carpeta abierta de Plugins
|
|
24
24
|
5. **Reinicia Roblox Studio**
|
|
@@ -27,7 +27,7 @@ Nota:
|
|
|
27
27
|
|
|
28
28
|
Despues de reiniciar, aparecera el boton **WROX** en Plugins.
|
|
29
29
|
|
|
30
|
-

|
|
31
31
|
|
|
32
32
|
## 4. Conectar con el Agente de IA
|
|
33
33
|
|
|
@@ -52,13 +52,13 @@ El servidor MCP debe estar instalado. Completa primero la guia de tu app de IA:
|
|
|
52
52
|
3. Clic en **Connect**
|
|
53
53
|
4. Cuando veas **"Connected"**, listo
|
|
54
54
|
|
|
55
|
-

|
|
56
56
|
|
|
57
57
|
## 5. Configuracion (Opcional)
|
|
58
58
|
|
|
59
59
|
Usa el boton de configuracion en la esquina superior derecha.
|
|
60
60
|
|
|
61
|
-

|
|
61
|
+

|
|
62
62
|
|
|
63
63
|
- **Conexion automatica**
|
|
64
64
|
- **Reconexion automatica**
|
package/docs/es/pro-upgrade.md
CHANGED
|
@@ -48,7 +48,7 @@ Solo necesitas activar la licencia una vez, ya sea en el plugin o en el dashboar
|
|
|
48
48
|
5. Si el estado no se actualiza de inmediato, haz clic en **Refresh**.
|
|
49
49
|
6. Cuando la activación se complete, el estado cambiará de Basic a Pro y las funciones Pro quedarán disponibles.
|
|
50
50
|
|
|
51
|
-

|
|
52
52
|
|
|
53
53
|
### Activar en el dashboard
|
|
54
54
|
|
|
@@ -58,7 +58,7 @@ Solo necesitas activar la licencia una vez, ya sea en el plugin o en el dashboar
|
|
|
58
58
|
4. Haz clic en **Activate License** para activar la licencia.
|
|
59
59
|
5. Si hace falta, usa **Refresh License** para obtener el estado más reciente.
|
|
60
60
|
|
|
61
|
-

|
|
62
62
|
|
|
63
63
|
### Después de activar
|
|
64
64
|
|
package/docs/es/sync/overview.md
CHANGED
|
@@ -12,7 +12,7 @@ Sin Sync, la IA solo ve fragmentos pegados en el chat. Con Sync activo, trabaja
|
|
|
12
12
|
|
|
13
13
|
## Como funciona
|
|
14
14
|
|
|
15
|
-

|
|
15
|
+

|
|
16
16
|
|
|
17
17
|
1. Full Sync: espejo inicial del arbol/instancias de Studio a local
|
|
18
18
|
2. Incremental Sync: reflejo continuo de cambios nuevos
|
|
@@ -109,7 +109,7 @@ En Pro puedes controlar Direction y Apply Mode por tipo.
|
|
|
109
109
|
|
|
110
110
|
Cuando se detectan cambios tanto en Studio como en local durante la sincronizacion bidireccional, aparece un dialogo de resolucion de conflictos.
|
|
111
111
|
|
|
112
|
-
](../../assets/screenshots/plugin/sync/sync-conflict.png)
|
|
113
113
|
|
|
114
114
|
- **Studio Priority**: sobrescribir usando el estado de Studio como fuente de verdad
|
|
115
115
|
- **Local Priority**: aplicar los archivos locales a Studio
|
package/docs/id/README.md
CHANGED
|
@@ -79,7 +79,7 @@ Ini bukan sekadar generate kode. Ini adalah **aksi yang benar-benar bisa dieksek
|
|
|
79
79
|
|
|
80
80
|
AI bekerja dari mirror lokal yang tersinkron, jadi perubahan lintas banyak file tetap konsisten.
|
|
81
81
|
|
|
82
|
-

|
|
82
|
+

|
|
83
83
|
|
|
84
84
|
- Basic: sinkronisasi satu arah (Studio -> Local)
|
|
85
85
|
- Pro: sinkronisasi dua arah + Direction/Apply Mode per tipe + riwayat + multiplace
|
|
@@ -92,6 +92,8 @@ AI bisa mengontrol playtest Studio secara langsung. AI dapat memulai dan menghen
|
|
|
92
92
|
- "Tulis dan jalankan test untuk memastikan SpawnLocation berada di atas tanah."
|
|
93
93
|
- "Validasi lewat playtest bahwa script yang baru saya ubah berjalan tanpa error."
|
|
94
94
|
|
|
95
|
+

|
|
96
|
+
|
|
95
97
|
### 4) WROX Dashboard: pantau pekerjaan AI secara real-time
|
|
96
98
|
|
|
97
99
|
Dashboard berbasis web yang disediakan server MCP memungkinkan Anda melihat status koneksi, riwayat eksekusi alat, status sinkronisasi, dan riwayat perubahan game secara real-time.
|
|
@@ -18,7 +18,7 @@ Catatan:
|
|
|
18
18
|
2. Klik tab **Plugins**
|
|
19
19
|
3. Klik **Plugins Folder**
|
|
20
20
|
|
|
21
|
-

|
|
22
22
|
|
|
23
23
|
4. **Salin** `WeppyRobloxMCP.rbxm` ke folder Plugins
|
|
24
24
|
5. **Restart Roblox Studio**
|
|
@@ -27,7 +27,7 @@ Catatan:
|
|
|
27
27
|
|
|
28
28
|
Setelah restart, tombol **WROX** muncul di tab Plugins.
|
|
29
29
|
|
|
30
|
-

|
|
31
31
|
|
|
32
32
|
## 4. Hubungkan AI Agent
|
|
33
33
|
|
|
@@ -52,13 +52,13 @@ MCP server harus sudah terinstal. Selesaikan dulu panduan app AI yang Anda pakai
|
|
|
52
52
|
3. Klik **Connect**
|
|
53
53
|
4. Jika status **"Connected"** tampil, koneksi berhasil
|
|
54
54
|
|
|
55
|
-

|
|
56
56
|
|
|
57
57
|
## 5. Pengaturan (Opsional)
|
|
58
58
|
|
|
59
59
|
Gunakan tombol pengaturan di kanan atas plugin.
|
|
60
60
|
|
|
61
|
-

|
|
61
|
+

|
|
62
62
|
|
|
63
63
|
- **Auto Connect**
|
|
64
64
|
- **Auto Reconnect**
|
package/docs/id/pro-upgrade.md
CHANGED
|
@@ -48,7 +48,7 @@ Kamu hanya perlu mengaktifkan lisensi sekali, baik di plugin maupun di dashboard
|
|
|
48
48
|
5. Jika status belum langsung diperbarui, klik **Refresh**.
|
|
49
49
|
6. Setelah aktivasi berhasil, status berubah dari Basic ke Pro dan fitur Pro bisa digunakan.
|
|
50
50
|
|
|
51
|
-

|
|
52
52
|
|
|
53
53
|
### Aktifkan di dashboard
|
|
54
54
|
|
|
@@ -58,7 +58,7 @@ Kamu hanya perlu mengaktifkan lisensi sekali, baik di plugin maupun di dashboard
|
|
|
58
58
|
4. Klik **Activate License** untuk mengaktifkan lisensi.
|
|
59
59
|
5. Jika perlu, gunakan **Refresh License** untuk mengambil status terbaru.
|
|
60
60
|
|
|
61
|
-

|
|
62
62
|
|
|
63
63
|
### Setelah aktivasi
|
|
64
64
|
|
package/docs/id/sync/overview.md
CHANGED
|
@@ -12,7 +12,7 @@ Tanpa Sync, AI hanya melihat potongan kode yang ditempel di chat. Dengan Sync ak
|
|
|
12
12
|
|
|
13
13
|
## Cara kerjanya
|
|
14
14
|
|
|
15
|
-

|
|
15
|
+

|
|
16
16
|
|
|
17
17
|
1. Full Sync: mirror awal dari tree/instance Studio ke lokal
|
|
18
18
|
2. Incremental Sync: perubahan baru terus disinkronkan
|
|
@@ -109,7 +109,7 @@ Di Pro, Direction dan Apply Mode bisa diatur per tipe.
|
|
|
109
109
|
|
|
110
110
|
Ketika perubahan terdeteksi di sisi Studio maupun lokal selama sinkronisasi dua arah, dialog resolusi konflik akan muncul.
|
|
111
111
|
|
|
112
|
-
](../../assets/screenshots/plugin/sync/sync-conflict.png)
|
|
113
113
|
|
|
114
114
|
- **Studio Priority**: timpa dengan status Studio sebagai sumber kebenaran
|
|
115
115
|
- **Local Priority**: terapkan file lokal ke Studio
|
package/docs/ja/README.md
CHANGED
|
@@ -79,7 +79,7 @@ AIがStudio内で、スクリプト、インスタンス、プロパティ、地
|
|
|
79
79
|
|
|
80
80
|
同期されたローカルミラーを基準にAIが作業するため、複数ファイルにまたがる変更も一貫して適用できます。
|
|
81
81
|
|
|
82
|
-

|
|
82
|
+

|
|
83
83
|
|
|
84
84
|
- Basic: 片方向同期(Studio -> Local)
|
|
85
85
|
- Pro: 双方向同期 + タイプ別Direction/Apply Mode + 変更履歴 + マルチPlace
|
|
@@ -92,6 +92,8 @@ AIがStudioプレイテストを直接制御します。Play(F5)/Run(F8)
|
|
|
92
92
|
- 「SpawnLocationが地面の上にあることを確認するテストを書いて実行して。」
|
|
93
93
|
- 「今修正したスクリプトがエラーなしで動くかプレイテストで検証して。」
|
|
94
94
|
|
|
95
|
+

|
|
96
|
+
|
|
95
97
|
### 4) WROX Dashboard: AI作業をリアルタイムでモニタリング
|
|
96
98
|
|
|
97
99
|
MCPサーバーが提供するWebダッシュボードで、接続状態、ツール実行履歴、同期状態、ゲーム変更履歴をリアルタイムで確認します。
|
|
@@ -18,7 +18,7 @@ Roblox StudioでAIエージェントと連携するためのプラグインイ
|
|
|
18
18
|
2. 上部メニューの **Plugins** タブをクリック
|
|
19
19
|
3. **Plugins Folder** ボタンをクリック
|
|
20
20
|
|
|
21
|
-

|
|
22
22
|
|
|
23
23
|
4. 解凍フォルダ内の `WeppyRobloxMCP.rbxm` を Plugins フォルダへ**コピー**
|
|
24
24
|
5. **Roblox Studioを再起動**
|
|
@@ -27,7 +27,7 @@ Roblox StudioでAIエージェントと連携するためのプラグインイ
|
|
|
27
27
|
|
|
28
28
|
再起動後、Pluginsタブに **WROX** ボタンが表示されます。
|
|
29
29
|
|
|
30
|
-

|
|
31
31
|
|
|
32
32
|
## 4. AIエージェントに接続
|
|
33
33
|
|
|
@@ -52,13 +52,13 @@ MCPサーバーがインストールされている必要があります。利
|
|
|
52
52
|
3. プラグインウィンドウで **Connect** をクリック
|
|
53
53
|
4. **"Connected"** が表示されたら完了
|
|
54
54
|
|
|
55
|
-

|
|
56
56
|
|
|
57
57
|
## 5. 設定(任意)
|
|
58
58
|
|
|
59
59
|
右上の設定ボタンからオプションを変更できます。
|
|
60
60
|
|
|
61
|
-

|
|
61
|
+

|
|
62
62
|
|
|
63
63
|
- **自動接続**: Studio起動時に自動接続
|
|
64
64
|
- **自動再接続**: 切断時に自動再接続
|
package/docs/ja/pro-upgrade.md
CHANGED
|
@@ -48,7 +48,7 @@ StudioのプレイテストをAIが実行・検証します。F5(Play)/F8(Run)
|
|
|
48
48
|
5. 状態がすぐに更新されない場合は **Refresh** を押して再確認します。
|
|
49
49
|
6. 有効化が完了すると、状態がBasicからProに変わり、Pro機能を使えるようになります。
|
|
50
50
|
|
|
51
|
-

|
|
52
52
|
|
|
53
53
|
### ダッシュボードで有効化
|
|
54
54
|
|
|
@@ -58,7 +58,7 @@ StudioのプレイテストをAIが実行・検証します。F5(Play)/F8(Run)
|
|
|
58
58
|
4. **Activate License** を押してライセンスを有効化します。
|
|
59
59
|
5. 必要に応じて **Refresh License** で最新状態を再取得します。
|
|
60
60
|
|
|
61
|
-

|
|
62
62
|
|
|
63
63
|
### 有効化後の確認
|
|
64
64
|
|
package/docs/ja/sync/overview.md
CHANGED
|
@@ -12,7 +12,7 @@ Syncがない場合、AIはチャットに貼られた断片だけを見て判
|
|
|
12
12
|
|
|
13
13
|
## 基本の動作
|
|
14
14
|
|
|
15
|
-

|
|
15
|
+

|
|
16
16
|
|
|
17
17
|
1. Full Sync: Studioツリー/インスタンスをローカルに初期ミラー
|
|
18
18
|
2. Incremental Sync: 以後の変更分のみを継続反映
|
|
@@ -109,7 +109,7 @@ ProではタイプごとにDirection/Apply Modeを細かく制御できます。
|
|
|
109
109
|
|
|
110
110
|
双方向同期中にStudioとローカルの両方で変更が検出されると、コンフリクト解決ダイアログが表示されます。
|
|
111
111
|
|
|
112
|
-
](../../assets/screenshots/plugin/sync/sync-conflict.png)
|
|
113
113
|
|
|
114
114
|
- **Studio Priority**: Studio側の状態を基準に上書き
|
|
115
115
|
- **Local Priority**: ローカルファイルをStudioに反映
|
package/docs/ko/README.md
CHANGED
|
@@ -79,7 +79,7 @@ npx -y @weppy/roblox-mcp
|
|
|
79
79
|
|
|
80
80
|
AI가 로컬 동기화된 프로젝트를 기준으로 전체 구조를 이해해, 여러 파일에 걸친 변경을 일관되게 수행합니다.
|
|
81
81
|
|
|
82
|
-

|
|
82
|
+

|
|
83
83
|
|
|
84
84
|
- Basic: Studio -> Local 단방향 동기화
|
|
85
85
|
- Pro: 양방향 동기화 + 타입별 Direction/Apply Mode + 변경 기록 + 멀티 Place
|
|
@@ -92,6 +92,8 @@ Studio 플레이테스트를 AI가 직접 제어합니다. F5(Play)/F8(Run) 시
|
|
|
92
92
|
- "SpawnLocation이 지면 위에 있는지 테스트 스크립트를 작성해서 자동 실행해줘."
|
|
93
93
|
- "방금 수정한 스크립트가 에러 없이 동작하는지 플레이테스트로 검증해줘."
|
|
94
94
|
|
|
95
|
+

|
|
96
|
+
|
|
95
97
|
### 4) WROX Dashboard: AI 작업을 실시간으로 모니터링
|
|
96
98
|
|
|
97
99
|
MCP 서버가 제공하는 웹 대시보드에서 연결 상태, 도구 실행 기록, 동기화 상태, 게임 변경 이력을 실시간으로 확인합니다.
|
|
@@ -18,7 +18,7 @@ Roblox Studio에서 AI 에이전트와 연결하기 위한 플러그인 설치
|
|
|
18
18
|
2. 상단 메뉴 **Plugins** 탭 클릭
|
|
19
19
|
3. **Plugins Folder** 버튼 클릭
|
|
20
20
|
|
|
21
|
-

|
|
22
22
|
|
|
23
23
|
4. 압축 해제한 폴더에서 `WeppyRobloxMCP.rbxm` 파일을 열린 Plugins 폴더에 **복사**
|
|
24
24
|
5. **Roblox Studio 재시작**
|
|
@@ -27,7 +27,7 @@ Roblox Studio에서 AI 에이전트와 연결하기 위한 플러그인 설치
|
|
|
27
27
|
|
|
28
28
|
재시작 후 Plugins 탭에 **WROX** 버튼이 나타납니다.
|
|
29
29
|
|
|
30
|
-

|
|
31
31
|
|
|
32
32
|
## 4. AI 에이전트 연결
|
|
33
33
|
|
|
@@ -52,13 +52,13 @@ MCP 서버가 설치되어 있어야 합니다. 사용하는 AI 앱에 맞는
|
|
|
52
52
|
3. 플러그인 창에서 **Connect** 버튼 클릭
|
|
53
53
|
4. **"Connected"** 상태가 표시되면 연결 완료
|
|
54
54
|
|
|
55
|
-

|
|
56
56
|
|
|
57
57
|
## 5. 설정 (선택사항)
|
|
58
58
|
|
|
59
59
|
플러그인 우측 상단의 설정 버튼을 클릭하면 다양한 옵션을 변경할 수 있습니다.
|
|
60
60
|
|
|
61
|
-

|
|
61
|
+

|
|
62
62
|
|
|
63
63
|
- **자동 연결**: Studio 시작 시 자동으로 MCP 서버에 연결
|
|
64
64
|
- **자동 재연결**: 연결이 끊어지면 자동으로 재연결 시도
|
package/docs/ko/pro-upgrade.md
CHANGED
|
@@ -48,7 +48,7 @@ Studio 플레이테스트를 AI가 실행하고 검증합니다. F5(Play)/F8(Run
|
|
|
48
48
|
5. 상태가 바로 갱신되지 않으면 **Refresh** 버튼으로 다시 확인합니다.
|
|
49
49
|
6. 활성화가 완료되면 Basic 대신 Pro 상태로 표시되고 Pro 기능을 사용할 수 있습니다.
|
|
50
50
|
|
|
51
|
-

|
|
52
52
|
|
|
53
53
|
### 대시보드에서 활성화
|
|
54
54
|
|
|
@@ -58,7 +58,7 @@ Studio 플레이테스트를 AI가 실행하고 검증합니다. F5(Play)/F8(Run
|
|
|
58
58
|
4. **Activate License** 버튼을 눌러 라이선스를 활성화합니다.
|
|
59
59
|
5. 필요하면 **Refresh License**로 최신 상태를 다시 조회합니다.
|
|
60
60
|
|
|
61
|
-

|
|
62
62
|
|
|
63
63
|
### 활성화 후 확인
|
|
64
64
|
|
package/docs/ko/sync/overview.md
CHANGED
|
@@ -12,7 +12,7 @@ Sync가 없으면 AI는 대화에 붙여 넣은 코드 일부만 보고 판단
|
|
|
12
12
|
|
|
13
13
|
## 기본 동작 방식
|
|
14
14
|
|
|
15
|
-

|
|
15
|
+

|
|
16
16
|
|
|
17
17
|
1. Full Sync: Studio 트리와 인스턴스를 로컬 미러로 초기 동기화
|
|
18
18
|
2. Incremental Sync: 변경 감시로 이후 변경분만 반영
|
|
@@ -109,7 +109,7 @@ Pro에서는 타입별로 Direction/Apply Mode를 다르게 설정해 워크플
|
|
|
109
109
|
|
|
110
110
|
양방향 동기화 중 Studio와 로컬 양쪽에서 변경이 감지되면, 아래와 같은 충돌 해결 화면이 나타납니다.
|
|
111
111
|
|
|
112
|
-
](../../assets/screenshots/plugin/sync/sync-conflict.png)
|
|
113
113
|
|
|
114
114
|
- **Studio Priority**: Studio 쪽 상태를 기준으로 덮어쓰기
|
|
115
115
|
- **Local Priority**: 로컬 파일을 기준으로 Studio에 반영
|
package/docs/pt-br/README.md
CHANGED
|
@@ -79,7 +79,7 @@ Nao e apenas geracao de codigo. Sao **acoes executaveis para fluxo real de desen
|
|
|
79
79
|
|
|
80
80
|
A IA trabalha com um espelho local sincronizado, entao alteracoes em varios arquivos continuam consistentes.
|
|
81
81
|
|
|
82
|
-

|
|
82
|
+

|
|
83
83
|
|
|
84
84
|
- Basic: sincronizacao unidirecional (Studio -> Local)
|
|
85
85
|
- Pro: sincronizacao bidirecional + Direction/Apply Mode por tipo + historico + multiplace
|
|
@@ -92,6 +92,8 @@ A IA pode controlar diretamente o playtest do Studio. Ela pode iniciar e parar P
|
|
|
92
92
|
- "Escreva e execute um teste para confirmar que o SpawnLocation esta acima do chao."
|
|
93
93
|
- "Valide com playtest se o script que acabei de alterar roda sem erros."
|
|
94
94
|
|
|
95
|
+

|
|
96
|
+
|
|
95
97
|
### 4) WROX Dashboard: monitore as operacoes da IA em tempo real
|
|
96
98
|
|
|
97
99
|
No dashboard web fornecido pelo servidor MCP, acompanhe em tempo real o status de conexao, historico de execucao de ferramentas, status de sincronizacao e historico de alteracoes do jogo.
|
|
@@ -18,7 +18,7 @@ Nota:
|
|
|
18
18
|
2. Clique na aba **Plugins**
|
|
19
19
|
3. Clique em **Plugins Folder**
|
|
20
20
|
|
|
21
|
-

|
|
22
22
|
|
|
23
23
|
4. **Copie** `WeppyRobloxMCP.rbxm` para a pasta de Plugins
|
|
24
24
|
5. **Reinicie o Roblox Studio**
|
|
@@ -27,7 +27,7 @@ Nota:
|
|
|
27
27
|
|
|
28
28
|
Apos reiniciar, o botao **WROX** aparecera na aba Plugins.
|
|
29
29
|
|
|
30
|
-

|
|
31
31
|
|
|
32
32
|
## 4. Conectar ao Agente de IA
|
|
33
33
|
|
|
@@ -52,13 +52,13 @@ O servidor MCP deve estar instalado. Complete primeiro o guia do seu app de IA:
|
|
|
52
52
|
3. Clique em **Connect**
|
|
53
53
|
4. Quando aparecer **"Connected"**, esta pronto
|
|
54
54
|
|
|
55
|
-

|
|
56
56
|
|
|
57
57
|
## 5. Configuracoes (Opcional)
|
|
58
58
|
|
|
59
59
|
Use o botao de configuracoes no canto superior direito.
|
|
60
60
|
|
|
61
|
-

|
|
61
|
+

|
|
62
62
|
|
|
63
63
|
- **Conexao Automatica**
|
|
64
64
|
- **Reconexao Automatica**
|
|
@@ -48,7 +48,7 @@ Você só precisa ativar a licença uma vez, no plugin ou no dashboard. As duas
|
|
|
48
48
|
5. Se o status não atualizar imediatamente, clique em **Refresh**.
|
|
49
49
|
6. Quando a ativação terminar, o status muda de Basic para Pro e os recursos Pro ficam disponíveis.
|
|
50
50
|
|
|
51
|
-

|
|
52
52
|
|
|
53
53
|
### Ativar no dashboard
|
|
54
54
|
|
|
@@ -58,7 +58,7 @@ Você só precisa ativar a licença uma vez, no plugin ou no dashboard. As duas
|
|
|
58
58
|
4. Clique em **Activate License** para ativar a licença.
|
|
59
59
|
5. Se necessário, use **Refresh License** para buscar o status mais recente.
|
|
60
60
|
|
|
61
|
-

|
|
62
62
|
|
|
63
63
|
### Depois da ativação
|
|
64
64
|
|
|
@@ -12,7 +12,7 @@ Sem Sync, a IA so enxerga trechos colados no chat. Com Sync ativo, ela trabalha
|
|
|
12
12
|
|
|
13
13
|
## Como funciona
|
|
14
14
|
|
|
15
|
-

|
|
15
|
+

|
|
16
16
|
|
|
17
17
|
1. Full Sync: espelho inicial da arvore/instancias do Studio para local
|
|
18
18
|
2. Incremental Sync: atualizacao continua das mudancas novas
|
|
@@ -109,7 +109,7 @@ No Pro, voce controla Direction e Apply Mode por tipo.
|
|
|
109
109
|
|
|
110
110
|
Quando mudancas sao detectadas tanto no Studio quanto no local durante a sincronizacao bidirecional, um dialogo de resolucao de conflitos aparece.
|
|
111
111
|
|
|
112
|
-
](../../assets/screenshots/plugin/sync/sync-conflict.png)
|
|
113
113
|
|
|
114
114
|
- **Studio Priority**: sobrescrever usando o estado do Studio como fonte de verdade
|
|
115
115
|
- **Local Priority**: aplicar arquivos locais ao Studio
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weppy/roblox-mcp",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.10",
|
|
4
4
|
"description": "MCP (Model Context Protocol) server for Roblox Studio integration - enables AI coding agents to interact with Roblox Studio in real-time",
|
|
5
5
|
"main": "plugins/weppy-roblox-mcp/dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -123,7 +123,7 @@ data: ${JSON.stringify(n)}
|
|
|
123
123
|
`).filter(g=>g.length>0)}catch(h){if(h.code==="ENOENT"){n.status(200).json({entries:[],total:0,hasMore:!1});return}throw h}let p=[];for(let h=u.length-1;h>=0;h--)try{let g=JSON.parse(u[h]);if(o&&g.direction!==o||s&&g.type!==s)continue;p.push(g)}catch{continue}let d=p.length,m={entries:p.slice(a,a+i),total:d,hasMore:a+i<d};n.status(200).json(m)}getStatusSummary(){if(this.ctx.activeFullSyncPlaceId!==null&&this.ctx.activeFullSyncPlaceId!==void 0)return{active:!0,placeId:this.ctx.activeFullSyncPlaceId};for(let[e,n]of this.ctx.places.entries())if(n.state==="syncing")return{active:!0,placeId:e};return{active:!1}}getDirectionForCategory(e){let n;if(this.ctx.activeFullSyncPlaceId!==null&&this.ctx.activeFullSyncPlaceId!==void 0&&(n=this.ctx.places.get(this.ctx.activeFullSyncPlaceId)),!n){let i=this.ctx.getDefaultRuntimePlaceId();i!=null&&(n=this.ctx.places.get(i))}if(!n)return"forward";let r=e;return n.directions[r]??"forward"}getStatusDirect(e){let n=e!=null?parseInt(e,10):this.ctx.getDefaultRuntimePlaceId();if(n==null)return{state:"idle",instanceCount:0,scriptCount:0,lastFullSync:null,lastIncrementalSync:null,syncRoot:this.ctx.config.getSyncRoot(),activeClientId:null,reverseSyncAvailable:!1,modifiedFileCount:0,applyModes:{...Bi}};let r=this.ctx.places.get(n);if(!r)return{state:"idle",instanceCount:0,scriptCount:0,lastFullSync:null,lastIncrementalSync:null,syncRoot:this.ctx.config.getPlaceRoot(n),activeClientId:null,reverseSyncAvailable:!1,modifiedFileCount:0,applyModes:{...Bi}};let i=r.fileWatcher?.getPendingCount()??0;return{state:r.state,instanceCount:r.instanceCount,scriptCount:r.scriptCount,lastFullSync:r.lastFullSync,lastIncrementalSync:r.lastIncrementalSync,syncRoot:this.ctx.config.getPlaceRoot(n),activeClientId:r.activeClientId,reverseSyncAvailable:i>0,modifiedFileCount:i,applyModes:{...r.applyModes}}}getConfigDirect(){return this.ctx.config.getConfig()}async getHistoryDirect(e,n){let r=parseInt(e,10),i=this.ctx.places.get(r);i&&await i.writer.flushHistory();let a=Math.min(Math.max(n?.limit??50,1),200),o=Math.max(n?.offset??0,0),s=this.ctx.config.getHistoryPath(r),c=[];try{c=(await Mo.readFile(s,"utf-8")).split(`
|
|
124
124
|
`).filter(f=>f.length>0)}catch(d){if(d.code==="ENOENT")return{entries:[],total:0,hasMore:!1};throw d}let l=[];for(let d=c.length-1;d>=0;d--)try{l.push(JSON.parse(c[d]))}catch{continue}let u=l.length;return{entries:l.slice(o,o+a),total:u,hasMore:o+a<u}}getDirectionsDirect(e){let n=e!=null?parseInt(e,10):this.ctx.getDefaultRuntimePlaceId();if(n==null)return{...qi};let r=this.ctx.places.get(n);return r?{...r.directions}:{...qi}}getProgressDirect(e){let n=e!=null?parseInt(e,10):this.ctx.getDefaultRuntimePlaceId();if(n==null)return{state:"idle",isSyncing:!1};let r=this.ctx.places.get(n);if(!r)return{state:"idle",isSyncing:!1};if(!r.syncProgress)return{state:r.state,isSyncing:!1,lastSync:{instanceCount:r.instanceCount,scriptCount:r.scriptCount,completedAt:r.lastFullSync}};let i=r.syncProgress,a=Date.now()-i.syncStartTime,o=i.totalInstances>0?Math.min(100,Math.round(i.processedInstances/i.totalInstances*100)):0,s;if(i.processedInstances>0&&o<100){let l=a/i.processedInstances,u=i.totalInstances-i.processedInstances;s=Math.round(l*u)}let c=a>0?Math.round(i.bytesReceived/a*1e3):0;return{state:r.state,isSyncing:!0,progressPercent:o,currentService:i.currentService,currentChunk:{index:i.currentChunkIndex,total:i.currentTotalChunks},instances:{processed:i.processedInstances,total:i.totalInstances},services:{processed:i.processedServices,total:i.totalServices},elapsedMs:a,estimatedRemainingMs:s,bytesReceived:i.bytesReceived,bytesPerSecond:c}}async readSyncedFile(e,n){let r=parseInt(e,10),i=this.ctx.places.get(r);if(!i)throw new Error(`Place ${e} not found in sync cache`);let a=i.index.resolvePropsPath(n);try{return{content:await Mo.readFile(a,"utf-8"),path:a}}catch(s){if(s.code!=="ENOENT")throw s}for(let s of Eo){let c=i.index.resolveScriptPath(n,s,!1);try{return{content:await Mo.readFile(c,"utf-8"),path:c}}catch{continue}}let o=i.index.resolveValuePath(n);try{return{content:await Mo.readFile(o,"utf-8"),path:o}}catch(s){if(s.code!=="ENOENT")throw s}throw new Error(`No synced file found for instance: ${n}`)}async writeSyncedFile(e,n,r){let i=parseInt(e,10),a=this.ctx.places.get(i);if(!a)throw new Error(`Place ${e} not found in sync cache`);await a.writer.writeScript(n,"Script",r,!1)}async executeViaDisk(e,n){let r=this.ctx.getDefaultRuntimePlaceId();if(r==null)throw new Error("No active sync place for disk execution");let i=this.ctx.places.get(r);if(!i)throw new Error(`Place ${r} not found in sync cache`);switch(e){case"set_script_source":{let a=n.scriptType||n.className||"Script";return await i.writer.writeScript(n.path,a,n.source,!1),{success:!0,path:n.path}}case"set_property":return await i.writer.writeProps(n.path,{className:n.className||"Instance",name:Ct(n.path),properties:{[n.property]:n.value}}),{success:!0,path:n.path};default:throw new Error(`Disk execution not supported for action: ${e}`)}}};import $e from"path";import{randomUUID as AQ}from"crypto";import{promises as Xt}from"fs";pe();var $f=class{constructor(e){this.ctx=e}preserveLocalFilesMap=new Map;pendingServiceTrees=new Map;async handleInitStart(e,n,r){if(n.previousPlaceId!==void 0&&n.previousPlaceId!==e&&(y.info("Place promotion detected",{from:n.previousPlaceId,to:e}),await this.ctx.config.promotePlaceRoot(n.previousPlaceId,e,n.placeName),this.ctx.places.get(n.previousPlaceId)&&this.ctx.places.delete(n.previousPlaceId),this.ctx.activeFullSyncPlaceId===n.previousPlaceId&&(this.ctx.activeFullSyncPlaceId=null),this.ctx.clearRuntimePlaceIfMatch(n.previousPlaceId)),this.ctx.activeFullSyncPlaceId!==null&&this.ctx.activeFullSyncPlaceId!==void 0&&this.ctx.activeFullSyncPlaceId!==e){r.status(409).json({error:"Conflict",message:`Place ${this.ctx.activeFullSyncPlaceId} is currently syncing. Only one place can sync at a time.`});return}let i=await this.ctx.getOrCreatePlaceContext(e,n.placeName);if(i.activeClientId&&i.activeClientId!==n.clientId&&i.activeFullSyncSessionId!==null){r.status(409).json({error:"Conflict",message:`Another client (${i.activeClientId}) is currently syncing this place`});return}if(this.ctx.activeFullSyncPlaceId=e,this.ctx.touchRuntimePlace(e),i.activeClientId=n.clientId,i.placeName=n.placeName,n.directions&&typeof n.directions=="object"){let p=n.directions,d=f=>f==="forward"||f==="reverse"||f==="bidirectional";d(p.scripts)&&(i.directions.scripts=p.scripts),d(p.values)&&(i.directions.values=p.values),d(p.containers)&&(i.directions.containers=p.containers),d(p.data)&&(i.directions.data=p.data),d(p.services)&&(i.directions.services=p.services),y.info("Sync directions received",{placeId:e,directions:i.directions})}else i.directions={...qi};if(n.applyModes&&typeof n.applyModes=="object"){let p=n.applyModes,d=f=>f==="auto"||f==="manual";d(p.scripts)&&(i.applyModes.scripts=p.scripts),d(p.values)&&(i.applyModes.values=p.values),d(p.containers)&&(i.applyModes.containers=p.containers),d(p.data)&&(i.applyModes.data=p.data),d(p.services)&&(i.applyModes.services=p.services)}else i.applyModes={...Bi};i.forwardRestoreQueue=[];let a=AQ();i.activeFullSyncSessionId=a,this.pendingServiceTrees.set(a,new Map),i.instanceCount=0,i.scriptCount=0;let o=this.ctx.config.getPlaceRoot(e),s=$e.join(o,`explorer_tmp_${a}`);await Xt.mkdir(s,{recursive:!0}),i.tmpIndex=new Yr(o,s),i.tmpWriter=new jo(this.ctx.config,i.tmpIndex,e),i.collisionDirMap=new Map;let c=n.preserveLocalFiles;Array.isArray(c)&&c.length>0&&(this.setPreserveLocalFiles(a,c),y.info("PreserveLocalFiles set for sync",{syncId:a,fileCount:c.length}));let l={version:1,placeId:n.placeId,placeName:n.placeName,lastFullSync:null,lastIncrementalSync:null,instanceCount:0,scriptCount:0,syncMode:"mirror"},u=$e.join(o,".sync-meta.json");await Xt.mkdir($e.dirname(u),{recursive:!0}),await this.ctx.atomicWriteFile(u,JSON.stringify(l,null,2)+`
|
|
125
125
|
`),this.startTTLTimerForPlace(i,a),i.state="initializing",i.index.resetNameCounters(),i.syncProgress={syncStartTime:Date.now(),totalInstances:n.totalInstances,totalServices:n.totalServices,processedInstances:0,processedServices:0,currentService:null,currentChunkIndex:0,currentTotalChunks:0,bytesReceived:0,processedChunks:0},i.writer.appendChangeLog(`FULL_SYNC_START clientId=${n.clientId} placeId=${n.placeId}`),i.writer.appendHistory({timestamp:new Date().toISOString(),type:"fullSyncStart",direction:"forward",path:`place_${n.placeId}`,details:`services:${n.totalServices} instances:${n.totalInstances}`}),y.info("Full sync started",{syncId:a,clientId:n.clientId,placeId:n.placeId,placeName:n.placeName,totalServices:n.totalServices,totalInstances:n.totalInstances}),r.status(200).json({status:"started",syncId:i.activeFullSyncSessionId})}async handleInitChunk(e,n,r){let i=this.ctx.places.get(e);if(!i||!i.activeFullSyncSessionId||!i.tmpIndex||!i.tmpWriter){r.status(400).json({error:"No active sync session",message:"Call sync/init with phase=start first"});return}if(i.activeClientId&&n.clientId&&i.activeClientId!==n.clientId){r.status(409).json({error:"Conflict",message:`This sync session belongs to client ${i.activeClientId}`});return}let a=i.activeFullSyncSessionId,o=this.getOrCreatePendingServiceTree(a,n),s=0,c=i.collisionDirMap;for(let u of n.instances){if(this.ctx.config.isForbiddenPath(u.path))continue;let p=i.tmpIndex.resolveParentDir(u.path),d=c.get(p)??p,{resolved:f,retroactiveRename:m}=i.tmpIndex.registerCollision(u.path,u.siblingIndex??void 0,d);m&&(await Qr(i.tmpIndex,d,m.from,m.to),this.rewritePendingEffectivePaths(o,i.tmpIndex.getExplorerRoot(),$e.join(d,m.from),$e.join(d,m.to)));let h=i.tmpIndex.resolveChildrenDir(u.path),g=$e.join(d,f);g!==h&&c.set(h,g);let x=i.tmpIndex.sanitizeName(u.name),w=u;if(d!==p||f!==x){let k=i.tmpIndex.getExplorerRoot(),I=$e.relative(k,d).split($e.sep).filter(H=>H.length>0),O=Xe(["game",...I,f]);w={...u,path:O}}let _=await i.tmpWriter.writeInstance(w);i.tmpIndex.setClassName(u.path,u.className,u.siblingIndex),s++,(_.propsWritten||_.valueWritten)&&i.instanceCount++,_.scriptWritten&&i.scriptCount++,o.instances.push({effectivePath:w.path,originalPath:u.path,className:u.className})}let l=this.isLastChunk(o,n.chunkIndex,n.totalChunks);if(l){let u=this.buildServiceTree(i,o);await i.tmpWriter.writeTree(n.serviceName,u);let p=this.pendingServiceTrees.get(a);p?.delete(n.serviceName),p&&p.size===0&&this.pendingServiceTrees.delete(a)}i.syncProgress&&(i.syncProgress.processedInstances+=s,i.syncProgress.currentService=n.serviceName,i.syncProgress.currentChunkIndex=n.chunkIndex,i.syncProgress.currentTotalChunks=n.totalChunks,i.syncProgress.processedChunks++,i.syncProgress.bytesReceived+=JSON.stringify(n).length,l&&i.syncProgress.processedServices++),y.debug("Sync chunk processed",{placeId:e,serviceName:n.serviceName,chunkIndex:n.chunkIndex,totalChunks:n.totalChunks,processed:s}),r.status(200).json({processed:s,service:n.serviceName})}async handleInitComplete(e,n,r){let i=this.ctx.places.get(e);if(!i||!i.activeFullSyncSessionId){r.status(400).json({error:"No active sync session",message:"Call sync/init with phase=start first"});return}if(i.activeClientId&&n.clientId&&i.activeClientId!==n.clientId){r.status(409).json({error:"Conflict",message:`This sync session belongs to client ${i.activeClientId}`});return}let a=i.activeFullSyncSessionId,o=this.ctx.config.getPlaceRoot(e),s=$e.join(o,"explorer"),c=$e.join(o,`explorer_tmp_${a}`),l=this.getAndClearPreserveLocalFiles(a),u=new Map,p=[];if(l.length>0){for(let m of l){let h=$e.resolve(o,m);try{let g=await Xt.readFile(h,"utf-8");u.set(m,g)}catch{p.push(m)}}y.info("Backed up local files for preservation",{placeId:e,requested:l.length,backed:u.size,deleted:p.length})}try{await Xt.rm(s,{recursive:!0,force:!0})}catch{}await Xt.rename(c,s),i.instanceCount=n.instanceCount,i.scriptCount=n.scriptCount,i.lastFullSync=new Date().toISOString();let d={version:1,placeId:i.placeId,placeName:i.placeName,lastFullSync:i.lastFullSync,lastIncrementalSync:i.lastIncrementalSync,instanceCount:i.instanceCount,scriptCount:i.scriptCount,syncMode:"mirror"},f=$e.join(o,".sync-meta.json");if(await this.ctx.atomicWriteFile(f,JSON.stringify(d,null,2)+`
|
|
126
|
-
`),this.clearTTLTimerForPlace(i,a),i.index.clearAllHashes(),i.index.clearClassMappings(),i.tmpIndex){let m=i.tmpIndex.getExplorerRoot(),h=i.index.getExplorerRoot();for(let[g,x]of i.tmpIndex.getAllHashes()){let w=$e.relative(m,g),_=$e.resolve(h,w);i.index.updateHashByValue(_,x)}for(let[g,x]of i.tmpIndex.getAllFileHashes()){let w=$e.relative(m,g),_=$e.resolve(h,w);i.index.updateFileHashByValue(_,x)}i.index.resetNameCounters(),i.index.mergeNameMappingsFrom(i.tmpIndex)}if(i.tmpIndex=null,i.tmpWriter&&(i.tmpWriter.stopChangeLogFlusher(),i.tmpWriter=null),this.pendingServiceTrees.delete(a),i.collisionDirMap=null,await i.index.saveToDisk(),i.state="syncing",i.activeFullSyncSessionId=null,i.syncProgress=null,this.ctx.touchRuntimePlace(e),this.ctx.activeFullSyncPlaceId=null,await this.ctx.startFileWatcherForPlace(i),i.fileWatcher&&await i.fileWatcher.waitUntilReady(),u.size>0){for(let[m,h]of u){let g=$e.resolve(o,m);try{await Xt.mkdir($e.dirname(g),{recursive:!0}),await Xt.writeFile(g,h,"utf-8")}catch(x){y.warn("Failed to restore preserved file",{path:m,error:x instanceof Error?x.message:String(x)})}}y.info("Restored preserved local files",{placeId:e,count:u.size})}if(p.length>0){let m=0;for(let h of p){let g=$e.resolve(o,h);try{await Xt.unlink(g),i.index.removeHash(g),m++}catch(x){x.code!=="ENOENT"&&y.warn("Failed to delete preserved-as-deleted file",{path:h,error:x instanceof Error?x.message:String(x)})}}m>0&&(await i.index.saveToDisk(),y.info("Deleted locally-removed files from new sync",{placeId:e,count:m}))}i.writer.appendChangeLog(`FULL_SYNC_COMPLETE instances=${i.instanceCount} scripts=${i.scriptCount}`),i.writer.appendHistory({timestamp:new Date().toISOString(),type:"fullSyncComplete",direction:"forward",path:`place_${e}`,details:`instances:${i.instanceCount} scripts:${i.scriptCount}`}),y.info("Full sync completed",{placeId:e,instanceCount:i.instanceCount,scriptCount:i.scriptCount}),r.status(200).json({status:"completed",instanceCount:i.instanceCount,scriptCount:i.scriptCount,syncRoot:this.ctx.config.getPlaceRoot(e)})}setPreserveLocalFiles(e,n){this.preserveLocalFilesMap.set(e,n)}getAndClearPreserveLocalFiles(e){let n=this.preserveLocalFilesMap.get(e)||[];return this.preserveLocalFilesMap.delete(e),n}clearPreserveLocalFiles(e){this.preserveLocalFilesMap.delete(e)}clearPendingServiceTrees(e){this.pendingServiceTrees.delete(e)}startTTLTimerForPlace(e,n){let r=setTimeout(async()=>{y.warn("Incomplete sync TTL expired, cleaning up",{placeId:e.placeId,syncId:n});let i=this.ctx.config.getPlaceRoot(e.placeId),a=$e.join(i,`explorer_tmp_${n}`);try{await Xt.rm(a,{recursive:!0,force:!0})}catch(o){y.error("Failed to clean up expired temp dir",o instanceof Error?o:new Error(String(o)))}e.incompleteSyncTimer=null,e.activeFullSyncSessionId===n&&(e.activeFullSyncSessionId=null,e.activeClientId=null,e.state="idle",e.tmpIndex=null,this.pendingServiceTrees.delete(n),e.collisionDirMap=null,e.tmpWriter&&(e.tmpWriter.stopChangeLogFlusher(),e.tmpWriter=null),this.ctx.activeFullSyncPlaceId===e.placeId&&(this.ctx.activeFullSyncPlaceId=null),this.ctx.clearRuntimePlaceIfMatch(e.placeId))},vN);r&&typeof r=="object"&&"unref"in r&&r.unref(),e.incompleteSyncTimer=r}getOrCreatePendingServiceTree(e,n){let r=this.pendingServiceTrees.get(e);r||(r=new Map,this.pendingServiceTrees.set(e,r));let i=r.get(n.serviceName);if(i)return i;let a={serviceName:n.tree?.name??n.serviceName,serviceClassName:n.tree?.className??n.serviceClassName,zeroBasedChunkIndex:n.chunkIndex===0,instances:[]};return r.set(n.serviceName,a),a}isLastChunk(e,n,r){return r<=1?!0:e.zeroBasedChunkIndex?n>=r-1:n>=r}buildServiceTree(e,n){let r={name:n.serviceName,className:n.serviceClassName,childCount:0,children:[],syncedAt:new Date().toISOString()};for(let i of n.instances){let a=this.resolveEffectiveSegments(e,i.effectivePath),o=gt(i.originalPath);this.upsertTreeNode(r,a,o,i.className)}return this.recomputeTreeChildCounts(r),r.syncedAt=new Date().toISOString(),r}resolveEffectiveSegments(e,n){return e.tmpIndex?gt(n).map(r=>e.tmpIndex.sanitizeName(r)):gt(n)}rewritePendingEffectivePaths(e,n,r,i){let a=$e.relative(n,r).split($e.sep).filter(l=>l.length>0),o=$e.relative(n,i).split($e.sep).filter(l=>l.length>0);if(a.length===0||o.length===0)return;let s=Xe(["game",...a]),c=Xe(["game",...o]);for(let l of e.instances){if(l.effectivePath===s){l.effectivePath=c;continue}(l.effectivePath.startsWith(`${s}.`)||l.effectivePath.startsWith(`${s}[`))&&(l.effectivePath=`${c}${l.effectivePath.slice(s.length)}`)}}upsertTreeNode(e,n,r,i){if(n.length<=1)return;let a=e.children;for(let o=1;o<n.length;o++){let s=n[o],c=r[o],l=o===n.length-1,u=a.find(p=>p.name===s);u?l&&(u.className=i,c!==void 0&&c!==s&&(u.originalName=c)):(u={name:s,className:l?i:"Folder",childCount:0,children:[]},c!==void 0&&c!==s&&(u.originalName=c),a.push(u)),u.children||(u.children=[]),a=u.children}}recomputeTreeChildCounts(e){let n=r=>{let i=r.children??[];r.children=i;for(let a of i)n(a);r.childCount=i.length};for(let r of e.children)n(r);e.childCount=e.children.length}clearTTLTimerForPlace(e,n){e.incompleteSyncTimer&&e.activeFullSyncSessionId===n&&(clearTimeout(e.incompleteSyncTimer),e.incompleteSyncTimer=null)}async cleanupStaleTempDirs(){let e=this.ctx.config.getSyncRoot();try{let n=await Xt.readdir(e,{withFileTypes:!0});for(let r of n)if(r.isDirectory()){if(r.name.startsWith("explorer_tmp_")){let i=$e.join(e,r.name);y.warn("Removing stale temp directory from crashed sync",{dir:r.name});try{await Xt.rm(i,{recursive:!0,force:!0})}catch(a){y.error(`Failed to remove stale temp dir: ${r.name}`,a instanceof Error?a:new Error(String(a)))}}if(r.name.startsWith("place_")){let i=$e.join(e,r.name);try{let a=await Xt.readdir(i,{withFileTypes:!0});for(let o of a)if(o.isDirectory()&&o.name.startsWith("explorer_tmp_")){let s=$e.join(i,o.name);y.warn("Removing stale temp directory from crashed sync",{dir:`${r.name}/${o.name}`});try{await Xt.rm(s,{recursive:!0,force:!0})}catch(c){y.error(`Failed to remove stale temp dir: ${r.name}/${o.name}`,c instanceof Error?c:new Error(String(c)))}}}catch{continue}}}}catch(n){if(n.code==="ENOENT")return;y.warn("Failed to scan for stale temp dirs",{error:n instanceof Error?n.message:String(n)})}}};import Gn from"path";import{promises as kN}from"fs";pe();var Cf=class{constructor(e){this.ctx=e}async handleReversePending(e,n){let r=this.ctx.resolveQueryPlaceId(e);if(r==null){n.status(200).json({pending:0,hasConflicts:!1,lastDetected:null});return}let i=this.ctx.places.get(r);if(!i){n.status(404).json({error:"Place not found",message:`No sync context for place ${r}`});return}let a={pending:i.fileWatcher?.getPendingCount()??0,hasConflicts:!1,lastDetected:i.fileWatcher?.getLastDetected()??null,forwardRestoreNeeded:i.forwardRestoreQueue.length};this.ctx.touchRuntimePlace(r),n.status(200).json(a)}async handleReverseSyncChanges(e,n){let r=this.ctx.resolveQueryPlaceId(e);if(r==null){n.status(200).json({changes:[],count:0});return}let i=this.ctx.places.get(r);if(!i){n.status(404).json({error:"Place not found",message:`No sync context for place ${r}`});return}if(i.state!=="syncing"){n.status(400).json({error:"Not syncing",message:"Reverse sync is only available when sync is active"});return}let a=i.fileWatcher?.drainPendingChanges()??[],o=a.length>0?await i.reader.buildChangesFromPending(a):[];this.ctx.touchRuntimePlace(r),n.status(200).json({changes:o,count:o.length})}async handleReverseSyncResult(e,n){let r=e.body,i=r.placeId??this.ctx.getDefaultRuntimePlaceId();if(i==null){n.status(400).json({error:"Validation error",message:"placeId is required (in body or via active sync session)"});return}let a=r.appliedFiles??r.appliedPaths;if(!a||!Array.isArray(a)){n.status(400).json({error:"Validation error",message:"appliedFiles (or appliedPaths) must be an array of relative file paths"});return}let o=this.ctx.places.get(i);if(!o){n.status(404).json({error:"Place not found",message:`No sync context for place ${i}`});return}this.ctx.touchRuntimePlace(i);let s=this.ctx.config.getPlaceRoot(i),c=0,l=[];for(let u of a){let p=Gn.resolve(s,u);if(!kf(s,p)){l.push({path:u,error:"Path is outside the place root"});continue}try{let d=await kN.readFile(p,"utf-8"),f=o.index.computeHash(d);o.index.updateHashByValue(p,f),o.index.updateFileHashByValue(p,f),c++}catch(d){let f=d.code;if(f==="ENOENT"){o.index.removeHash(p),o.index.removeHashesUnder(p);let m=this.resolveInstancePathForAppliedPath(o.index,p);if(m){let h=Ct(m),g=Et(m);g&&h&&await o.writer.removeFromTree(g,h)}c++}else f==="EISDIR"?c++:l.push({path:u,error:d instanceof Error?d.message:String(d)})}}c>0&&await o.index.saveToDisk(),c>0&&o.writer.appendHistory({timestamp:new Date().toISOString(),type:"reverseApply",direction:"reverse",path:`place_${i}`,details:`applied:${c} failed:${l.length}`}),n.status(200).json({updated:c,failed:l.length,errors:l})}async handleResolveConflict(e,n){let r=e.body;if(!r.fsPath||!r.resolution){n.status(400).json({error:"Validation error",message:"fsPath and resolution are required"});return}let{fsPath:i,resolution:a}=r,o=this.ctx.config.getSyncRoot();if(!kf(o,Gn.resolve(o,i))){n.status(403).json({error:"Forbidden",message:"Path is outside the sync root"});return}if(a==="skip"){n.status(200).json({status:"skipped",fsPath:i});return}let s;if(r.placeId&&(s=this.ctx.places.get(r.placeId)),!s){let d=i.match(/^place_(\d+)(?:_[^/]+)?\//);if(d){let f=parseInt(d[1],10);s=this.ctx.places.get(f)}else s=Array.from(this.ctx.places.values())[0]}if(!s){n.status(404).json({error:"No active place context",message:"No sync session is active. Start a sync first."});return}let c=this.ctx.config.getPlaceRoot(s.placeId),l=Gn.resolve(c,i);if(!kf(c,l)){n.status(403).json({error:"Forbidden",message:"Path is outside the place root"});return}let u=s.index,p=s.reader;if(a==="apply-studio"){u.resolveFile(l,"apply-studio"),await u.saveToDisk(),n.status(200).json({status:"resolved",resolution:"apply-studio",fsPath:i});return}if(a==="apply-file"){let d=await kN.readFile(l,"utf-8"),f=u.computeHash(d);u.resolveFile(l,"apply-file",f),await u.saveToDisk();let m=p.getFileType(l),h=p.resolveInstancePathFromFile(l);n.status(200).json({status:"resolved",resolution:"apply-file",fsPath:i,instancePath:h,fileType:m,content:d});return}n.status(400).json({error:"Invalid resolution",message:`Unknown resolution: ${a}`})}async handleReverseRescan(e,n){let r=this.ctx.resolveQueryPlaceId(e);if(r==null){n.status(200).json({added:0});return}let i=this.ctx.places.get(r);if(!i){n.status(404).json({error:"Place not found",message:`No sync context for place ${r}`});return}if(!i.fileWatcher){n.status(200).json({added:0});return}let a=await i.fileWatcher.rescan();this.ctx.touchRuntimePlace(r),y.info("Reverse rescan completed",{placeId:r,added:a}),n.status(200).json({added:a})}resolveInstancePathForAppliedPath(e,n){let r=e.resolveInstancePathFromFsPath(n);if(r)return r;let i=e.getExplorerRoot(),a=Gn.relative(i,n);if(a.startsWith("..")||a===""||Gn.isAbsolute(a))return null;let o=a.split(Gn.sep).filter(f=>f.length>0);if(o.length<2)return null;let s=Gn.basename(n),c=Gn.dirname(n),l=e.getOriginalInstance(c,s);if(l)return l.instancePath;let u=s.toLowerCase();if(Co.some(f=>u.endsWith(f))||u==="_tree.json")return null;let p=["game"],d=i;for(let f of o){d=Gn.join(d,f);let m=Gn.dirname(d);p.push(e.getOriginalNameForDir(m,f))}return Xe(p)}};pe();function IN(t){if(!t||typeof t!="object")return;let e=t;if(Array.isArray(e.instances))for(let n of e.instances)Array.isArray(n.properties)&&n.properties.length===0&&(n.properties={}),Array.isArray(n.attributes)&&n.attributes.length===0&&(n.attributes={});if(Array.isArray(e.changes))for(let n of e.changes)Array.isArray(n.properties)&&n.properties.length===0&&(n.properties={}),Array.isArray(n.attributes)&&n.attributes.length===0&&(n.attributes={})}var Uo=class{config;places;apiHandler;changeProcessor;initHandler;reverseHandler;activeFullSyncPlaceId=null;activeRuntimeSyncPlaceId=null;constructor(e){this.config=new cf(e),this.apiHandler=new Pf(this),this.changeProcessor=new If(this),this.initHandler=new $f(this),this.reverseHandler=new Cf(this),this.places=new sf({max:3,dispose:(n,r)=>{y.info("Disposing place context (LRU eviction)",{placeId:r}),this.activeFullSyncPlaceId===r&&(this.activeFullSyncPlaceId=null),this.activeRuntimeSyncPlaceId===r&&(this.activeRuntimeSyncPlaceId=null),n.fileWatcher&&(n.fileWatcher.stop().catch(i=>{y.error("Error stopping file watcher during dispose",i)}),n.fileWatcher=null),n.writer.stopChangeLogFlusher(),n.incompleteSyncTimer&&(clearTimeout(n.incompleteSyncTimer),n.incompleteSyncTimer=null),n.index.saveToDisk().catch(i=>{y.error("Error saving index during dispose",i)}),n.activeFullSyncSessionId&&this.initHandler.clearPendingServiceTrees(n.activeFullSyncSessionId),n.tmpWriter&&(n.tmpWriter.stopChangeLogFlusher(),n.tmpWriter=null),n.tmpIndex=null,n.collisionDirMap=null}})}getSyncRoot(){return this.config.getSyncRoot()}async getOrCreatePlaceContext(e,n){let r=this.places.get(e);if(r&&n){let i=this.config.getPlaceRoot(e),a=await this.config.resolvePlaceRoot(e,n);a!==i&&(y.info("Place root migrated, recreating context",{placeId:e,from:i,to:a}),this.places.delete(e),r=void 0)}if(!r){let i=await this.config.resolvePlaceRoot(e,n),a=Yw.join(i,"explorer");await Lo.mkdir(i,{recursive:!0}),await Lo.mkdir(a,{recursive:!0});let o=new Yr(i,a);await o.loadFromDisk();let s=new jo(this.config,o,e),c=new pf(this.config,o,i);s.startChangeLogFlusher(),r={placeId:e,placeName:"",index:o,writer:s,reader:c,fileWatcher:null,state:"idle",activeClientId:null,activeFullSyncSessionId:null,instanceCount:0,scriptCount:0,lastFullSync:null,lastIncrementalSync:null,tmpIndex:null,tmpWriter:null,incompleteSyncTimer:null,changesSinceLastSave:0,directions:{...qi},applyModes:{...Bi},forwardRestoreQueue:[],syncProgress:null,collisionDirMap:null},this.places.set(e,r),y.info("Created new place context",{placeId:e,placeRoot:i})}return r}async handleSyncInit(e,n){try{IN(e.body);let r=bN(e.body);if(!r.success){n.status(400).json({error:"Validation error",message:r.error});return}let i=r.data,a=i.phase==="start"?i.placeId??null:i.phase==="chunk"||i.phase==="complete"?this.activeFullSyncPlaceId:null;if(a==null){n.status(400).json({error:"Missing placeId",message:"placeId is required in start phase or must be set by previous start"});return}switch(i.phase){case"start":await this.initHandler.handleInitStart(a,i,n);break;case"chunk":await this.initHandler.handleInitChunk(a,i,n);break;case"complete":await this.initHandler.handleInitComplete(a,i,n);break}}catch(r){this.sendError(n,r,"handleSyncInit")}}async handleSyncUpdate(e,n){try{IN(e.body);let r=_N(e.body);if(!r.success){n.status(400).json({error:"Validation error",message:r.error});return}let i=r.data,a=i.placeId??this.getDefaultRuntimePlaceId();if(a==null){n.status(400).json({error:"Missing placeId",message:"placeId is required in body or must have an active sync session"});return}let o=await this.getOrCreatePlaceContext(a);if(o.activeFullSyncSessionId!==null||o.state==="initializing"){n.status(409).json({error:"Conflict",message:`Full sync in progress for place ${a}`});return}o.activeClientId=i.clientId,this.touchRuntimePlace(a);let s=yN(o,i.changes);y.info("Sync update received",{placeId:a,clientId:i.clientId,changeCount:s.length,receivedCount:i.changes.length,types:s.map(f=>f.type)});let c=[],l=[],u=0,p=new Map;for(let f of s)try{let m=await this.changeProcessor.processChangeForPlace(o,f,p);m?l.push(m):u++}catch(m){let h="path"in f?f.path:"oldPath"in f?f.oldPath:"unknown";c.push({path:h,error:m instanceof Error?m.message:String(m)})}for(let f of p.values())try{await _f(o,f)}catch(m){c.push({path:f.instancePath,error:m instanceof Error?m.message:String(m)})}o.changesSinceLastSave+=u,o.changesSinceLastSave>=xN&&(await o.index.saveToDisk(),o.changesSinceLastSave=0),o.lastIncrementalSync=new Date().toISOString();let d={processed:u,failed:c.length,errors:c,syncedAt:o.lastIncrementalSync};l.length>0&&(d.conflicts=l),n.status(200).json(d)}catch(r){this.sendError(n,r,"handleSyncUpdate")}}getDefaultRuntimePlaceId(){if(this.activeRuntimeSyncPlaceId!==null&&this.activeRuntimeSyncPlaceId!==void 0){if(this.places.has(this.activeRuntimeSyncPlaceId))return this.activeRuntimeSyncPlaceId;this.activeRuntimeSyncPlaceId=null}if(this.activeFullSyncPlaceId!==null&&this.activeFullSyncPlaceId!==void 0&&this.places.has(this.activeFullSyncPlaceId))return this.activeRuntimeSyncPlaceId=this.activeFullSyncPlaceId,this.activeRuntimeSyncPlaceId;for(let[e,n]of this.places.entries())if(n.state==="syncing"||n.state==="initializing")return this.activeRuntimeSyncPlaceId=e,e;return this.activeRuntimeSyncPlaceId=null,null}touchRuntimePlace(e){this.activeRuntimeSyncPlaceId=e}clearRuntimePlaceIfMatch(e){this.activeRuntimeSyncPlaceId===e&&(this.activeRuntimeSyncPlaceId=null)}resolveQueryPlaceId(e,n="runtime"){let r=e.query.placeId;if(r){let i=parseInt(r,10);if(!isNaN(i))return i}return n==="full"?this.activeFullSyncPlaceId:this.getDefaultRuntimePlaceId()}async handleSyncStatus(e,n){try{let r=this.resolveQueryPlaceId(e);if(r==null){let s={state:"idle",instanceCount:0,scriptCount:0,lastFullSync:null,lastIncrementalSync:null,syncRoot:this.config.getSyncRoot(),activeClientId:null,reverseSyncAvailable:!1,modifiedFileCount:0};n.status(200).json(s);return}let i=this.places.get(r);if(!i){let s={state:"idle",instanceCount:0,scriptCount:0,lastFullSync:null,lastIncrementalSync:null,syncRoot:this.config.getPlaceRoot(r),activeClientId:null,reverseSyncAvailable:!1,modifiedFileCount:0};n.status(200).json(s);return}let a=i.fileWatcher?.getPendingCount()??0;this.touchRuntimePlace(r);let o={state:i.state,instanceCount:i.instanceCount,scriptCount:i.scriptCount,lastFullSync:i.lastFullSync,lastIncrementalSync:i.lastIncrementalSync,syncRoot:this.config.getPlaceRoot(r),activeClientId:i.activeClientId,reverseSyncAvailable:a>0,modifiedFileCount:a,applyModes:i.applyModes,directions:i.directions,fileWatcherActive:i.fileWatcher!==null,forwardOnlyClasses:[...To]};n.status(200).json(o)}catch(r){this.sendError(n,r,"handleSyncStatus")}}async handleSyncStop(e,n){try{let r=e.body,i=r.placeId??this.getDefaultRuntimePlaceId();if(i==null){n.status(200).json({status:"idle",state:"idle",placeId:null,message:"No active sync place"});return}let a=this.places.get(i);if(!a){n.status(200).json({status:"idle",state:"idle",placeId:i,message:`No sync context for place ${i}`});return}if(r.clientId&&a.activeClientId&&r.clientId!==a.activeClientId&&(a.activeFullSyncSessionId!==null||a.state==="syncing"||a.state==="initializing")){n.status(409).json({error:"Conflict",message:`This sync session belongs to client ${a.activeClientId}`});return}let o=a.activeFullSyncSessionId;if(o&&(this.initHandler.clearPreserveLocalFiles(o),this.initHandler.clearPendingServiceTrees(o)),a.fileWatcher&&(await a.fileWatcher.stop(),a.fileWatcher=null),a.incompleteSyncTimer&&(clearTimeout(a.incompleteSyncTimer),a.incompleteSyncTimer=null),o){let s=this.config.getPlaceRoot(i),c=Yw.join(s,`explorer_tmp_${o}`);await Lo.rm(c,{recursive:!0,force:!0}).catch(()=>{})}a.tmpWriter&&(a.tmpWriter.stopChangeLogFlusher(),a.tmpWriter=null),a.tmpIndex=null,a.collisionDirMap=null,a.activeFullSyncSessionId=null,a.syncProgress=null,a.state="idle",a.activeClientId=null,a.instanceCount=0,a.scriptCount=0,this.activeFullSyncPlaceId===i&&(this.activeFullSyncPlaceId=null),this.clearRuntimePlaceIfMatch(i),y.info("Sync stopped",{placeId:i,reason:r.reason??"requested"}),n.status(200).json({status:"stopped",state:"idle",placeId:i})}catch(r){this.sendError(n,r,"handleSyncStop")}}async handleSyncConfig(e,n){try{if(e.method==="GET"){n.status(200).json(this.config.getConfig());return}let r=wN(e.body);if(!r.success){n.status(400).json({error:"Validation error",message:r.error});return}let i=r.data;i.maxDepth!==void 0&&this.config.updateConfig({maxDepth:i.maxDepth}),i.maxInstances!==void 0&&this.config.updateConfig({maxInstances:i.maxInstances}),n.status(200).json({status:"updated",config:this.config.getConfig()})}catch(r){this.sendError(n,r,"handleSyncConfig")}}async initialize(){try{await this.config.loadFromMeta(),await this.initHandler.cleanupStaleTempDirs(),y.info("SyncController initialized")}catch(e){y.error("SyncController initialization failed",e instanceof Error?e:new Error(String(e)))}}async shutdown(){try{this.places.clear(),y.info("SyncController shut down")}catch(e){y.error("SyncController shutdown error",e instanceof Error?e:new Error(String(e)))}}async atomicWriteFile(e,n){let r=e+".tmp."+NQ().slice(0,8);try{await Lo.writeFile(r,n,"utf-8"),await Lo.rename(r,e)}catch(i){throw await Lo.unlink(r).catch(()=>{}),i}}async handlePreCheck(e,n){try{await this.apiHandler.handlePreCheck(e,n)}catch(r){this.sendError(n,r,"handlePreCheck")}}async handleSyncDirections(e,n){try{await this.apiHandler.handleSyncDirections(e,n)}catch(r){this.sendError(n,r,"handleSyncDirections")}}async handleForwardRestoreList(e,n){try{await this.apiHandler.handleForwardRestoreList(e,n)}catch(r){this.sendError(n,r,"handleForwardRestoreList")}}async handleReversePending(e,n){try{await this.reverseHandler.handleReversePending(e,n)}catch(r){this.sendError(n,r,"handleReversePending")}}async handleReverseSyncChanges(e,n){try{await this.reverseHandler.handleReverseSyncChanges(e,n)}catch(r){this.sendError(n,r,"handleReverseSyncChanges")}}async handleReverseSyncResult(e,n){try{await this.reverseHandler.handleReverseSyncResult(e,n)}catch(r){this.sendError(n,r,"handleReverseSyncResult")}}async handleResolveConflict(e,n){try{await this.reverseHandler.handleResolveConflict(e,n)}catch(r){this.sendError(n,r,"handleResolveConflict")}}async handleReverseRescan(e,n){try{await this.reverseHandler.handleReverseRescan(e,n)}catch(r){this.sendError(n,r,"handleReverseRescan")}}async handleSyncHistory(e,n){try{await this.apiHandler.handleSyncHistory(e,n)}catch(r){this.sendError(n,r,"handleSyncHistory")}}async startFileWatcherForPlace(e){if(e.state!=="syncing"){y.debug("Skipping file watcher start - place not syncing",{placeId:e.placeId,state:e.state});return}e.fileWatcher&&await e.fileWatcher.stop();let n=Yw.join(this.config.getPlaceRoot(e.placeId),"explorer");e.fileWatcher=new bf(n,e.index),e.writer.setOnWriteCallback(r=>{e.fileWatcher?.suppressPath(r)}),e.fileWatcher.setDirectionChecker(r=>{let i=FA(r);return e.directions[i]}),e.fileWatcher.setOnForwardViolation(r=>{e.forwardRestoreQueue.includes(r)||(e.forwardRestoreQueue.push(r),y.info("Forward violation queued for restore",{placeId:e.placeId,relativePath:r,queueSize:e.forwardRestoreQueue.length}))}),await e.fileWatcher.start(),y.info("File watcher started for reverse sync",{placeId:e.placeId})}getStatusSummary(){return this.apiHandler.getStatusSummary()}getDirectionForCategory(e){return this.apiHandler.getDirectionForCategory(e)}getStatusDirect(e){return this.apiHandler.getStatusDirect(e)}getConfigDirect(){return this.apiHandler.getConfigDirect()}async getHistoryDirect(e,n){return this.apiHandler.getHistoryDirect(e,n)}getDirectionsDirect(e){return this.apiHandler.getDirectionsDirect(e)}getProgressDirect(e){return this.apiHandler.getProgressDirect(e)}async readSyncedFile(e,n){return this.apiHandler.readSyncedFile(e,n)}async writeSyncedFile(e,n,r){await this.apiHandler.writeSyncedFile(e,n,r)}async executeViaDisk(e,n){return this.apiHandler.executeViaDisk(e,n)}sendError(e,n,r){let i=n instanceof Error?n.message:String(n);if(i.includes("Path traversal detected")){e.status(403).json({error:"Forbidden",message:i});return}let a=n.code;if(a==="ENOSPC"||a==="EPERM"||a==="EACCES"){e.status(500).json({error:"Disk error",message:i});return}y.error(`SyncController.${r} failed`,n instanceof Error?n:new Error(i)),e.status(500).json({error:"Internal error",message:`${r}: ${i}`})}};import{randomUUID as OQ}from"crypto";function PN(t,e){let n=OQ(),r=n.replace(/-/g,"").substring(0,8).toUpperCase(),i=`${r.substring(0,4)}-${r.substring(4,8)}`;return{config:t,app:e,instanceId:n,sessionId:i,startTime:Date.now(),baseUrl:`http://${t.httpHost}:${t.httpPort}`,commandQueue:new Map,pendingCommands:new Map,globalPendingCommands:[],totalCommandsProcessed:0,pluginClients:new Map,mcpInstances:new Map,sseClients:new Set,cachedSelectionMap:new Map,isClientMode:!1,clientModeHealthTimer:null,clientModeConsecutiveHealthFailures:0,clientModeUpstreamReachable:!0,clientModeUpstreamContextCaptureEnabled:!0,clientModeLastHealthSuccessAt:null,clientModeLastHealthFailureAt:null,clientModeLastHealthError:null,historyManager:null,analyticsManager:null,executionContextManager:null,licenseState:null,syncController:null,internalActionExecutor:null,activeSyncOwnerInstanceId:null,activeProjectRoot:null,playtestControlCommand:null,aiClientName:"",pluginVersion:"",syncedSessionToken:null,serverLastCommandAt:null}}var $N=ri(xo(),1);pe();function CN(t){let e=$N.default.json({limit:"5mb"});t.app.use((n,r,i)=>{if(n.path.startsWith("/sync/")){i();return}e(n,r,i)}),t.app.use((n,r,i)=>{r.setHeader("Access-Control-Allow-Origin","http://localhost:3002"),r.setHeader("Access-Control-Allow-Methods","GET, POST, OPTIONS"),r.setHeader("Access-Control-Allow-Headers","Content-Type"),i()}),t.app.use((n,r,i)=>{y.debug(`${n.method} ${n.path}`,{ip:n.ip}),i()})}import{randomUUID as kl}from"crypto";pe();function DQ(){let t=process.env.WEPPY_ROBLOX_MCP_VERSION?.trim();return t||null}var We=DQ()??"2.0.9";Rf();function zN(t){let e=new Set;t.aiClientName&&e.add(t.aiClientName);for(let n of t.mcpInstances.values())n.aiClientName&&e.add(n.aiClientName);return Array.from(e)}function LQ(t){let n=Date.now();return Array.from(t.pluginClients.values()).filter(r=>n-r.lastSeen<1e4)}function AN(t){let n=Date.now();for(let[r,i]of t.mcpInstances)i.lastSeen&&n-i.lastSeen>15e3&&(t.mcpInstances.delete(r),i.sessionId&&t.executionContextManager?.endSession(i.sessionId),y.debug("Removed stale MCP instance",{instanceId:r,lastSeen:i.lastSeen}))}function NN(t,e,n){let r=e.query.clientId;if(r&&t.pluginClients.has(r)){let i=t.pluginClients.get(r);i.lastSeen=Date.now()}try{let i=e.body;if(!i||!Array.isArray(i.selection)||typeof i.count!="number"){y.warn("Invalid selection update request",{body:i}),n.status(400).json({error:"Invalid request body"});return}let a=r||"unknown",o=Date.now();t.cachedSelectionMap.set(a,{selection:i.selection,count:i.count,timestamp:o,clientId:a}),y.debug("Selection cache updated",{count:i.count,clientId:a,timestamp:o}),n.json({status:"ok",timestamp:o})}catch(i){y.error("Error handling selection update",i),n.status(500).json({error:"Internal server error"})}}function ON(t,e,n){let r=parseInt(e.query.maxAge)||3e4,i=Sl(t,r);i?n.json({cached:!0,...i}):n.json({cached:!1,message:"No cached selection available"})}function Sl(t,e=3e4,n){if(t.cachedSelectionMap.size===0)return null;let r;if(n)r=t.cachedSelectionMap.get(n);else for(let a of t.cachedSelectionMap.values())(!r||a.timestamp>r.timestamp)&&(r=a);if(!r)return null;let i=Date.now()-r.timestamp;return e===0||i<=e?r:null}function DN(t,e,n){try{let r=e.body;if(!r.clientId){n.status(400).json({error:"Missing clientId"});return}let i=t.pluginClients.get(r.clientId),a=Date.now(),o={clientId:r.clientId,projectName:r.projectName,placeName:r.placeName,placeId:r.placeId,pluginVersion:r.pluginVersion,connectedAt:i?.connectedAt||a,lastSeen:a,commandsProcessed:i?.commandsProcessed||0,connectionType:"polling"};t.pluginClients.set(r.clientId,o),t.pendingCommands.has(r.clientId)||t.pendingCommands.set(r.clientId,[]),r.pluginVersion&&(t.pluginVersion=r.pluginVersion),$t(t,"connection",{clientId:r.clientId,placeId:o.projectName,placeName:o.placeName,status:"connected"}),dl(t,{timestamp:new Date().toISOString(),type:"plugin",status:"connected",clientId:r.clientId,message:`Plugin connected \u2014 ${r.clientId}`,...o.placeId!==void 0?{placeId:o.placeId}:{},...o.placeName?{placeName:o.placeName}:{}}),t.analyticsManager&&(r.pluginVersion&&t.analyticsManager.setPluginVersion(r.pluginVersion),t.analyticsManager.trackPluginConnected()),y.info("Plugin client registered",{clientId:r.clientId,projectName:r.projectName,placeName:r.placeName,isReconnect:!!i}),n.json({status:"ok",clientId:r.clientId,serverInstanceId:t.instanceId,sessionId:t.sessionId,mcpVersion:We,connectedAt:o.connectedAt,aiClientNames:zN(t),serverStartTime:t.startTime,mcpInstanceCount:t.mcpInstances.size+1})}catch(r){y.error("Error registering plugin client",r),n.status(500).json({error:"Internal server error"})}}function MN(t,e,n){let r=e.body?.clientId;if(!r){n.status(400).json({error:"Missing clientId"});return}let i=t.pluginClients.has(r);t.pluginClients.delete(r),t.pendingCommands.delete(r),i&&($t(t,"connection",{clientId:r,status:"disconnected"}),dl(t,{timestamp:new Date().toISOString(),type:"plugin",status:"disconnected",clientId:r,message:`Plugin disconnected \u2014 ${r}`})),y.info("Plugin client unregistered",{clientId:r,existed:i}),n.json({status:"ok",existed:i})}function LN(t,e,n){try{let r=e.body;if(!r.instanceId){n.status(400).json({error:"Missing instanceId"});return}let i=Date.now(),a={instanceId:r.instanceId,...typeof r.sessionId=="string"?{sessionId:r.sessionId}:{},pid:r.pid,connectedAt:i,isServer:!1,lastSeen:i};r.aiClientName&&(a.aiClientName=r.aiClientName),r.cwd&&(a.cwd=r.cwd),"projectRoot"in r&&(a.projectRoot=r.projectRoot),t.mcpInstances.set(r.instanceId,a),$t(t,"mcp_status",{aiClientName:a.aiClientName??"Unknown",instanceId:r.instanceId,status:"registered"}),dl(t,{timestamp:new Date().toISOString(),type:"mcp",status:"registered",instanceId:r.instanceId,message:`MCP registered \u2014 ${a.aiClientName??r.instanceId}`,...a.aiClientName?{aiClientName:a.aiClientName}:{}}),y.info("MCP instance registered (client mode)",{instanceId:r.instanceId,pid:r.pid,cwd:r.cwd}),n.json({status:"ok",instanceId:r.instanceId,serverInstanceId:t.instanceId,sessionId:t.sessionId,mcpVersion:We,mcpInstanceCount:t.mcpInstances.size+1})}catch(r){y.error("Error registering MCP instance",r),n.status(500).json({error:"Internal server error"})}}function UN(t,e,n){let r=e.body?.instanceId;if(!r){n.status(400).json({error:"Missing instanceId"});return}let i=t.mcpInstances.get(r),a=!!i;t.mcpInstances.delete(r),i?.sessionId&&t.executionContextManager?.endSession(i.sessionId),a&&($t(t,"mcp_status",{aiClientName:i?.aiClientName??"Unknown",instanceId:r,status:"unregistered"}),dl(t,{timestamp:new Date().toISOString(),type:"mcp",status:"unregistered",instanceId:r,message:`MCP unregistered \u2014 ${i?.aiClientName??r}`,...i?.aiClientName?{aiClientName:i.aiClientName}:{}})),y.info("MCP instance unregistered",{instanceId:r,existed:a}),n.json({status:"ok",existed:a})}function FN(t,e,n){let{instanceId:r,aiClientName:i}=e.body;if(r&&i){let a=t.mcpInstances.get(r);a&&(a.aiClientName=i)}n.json({status:"ok"})}function qN(t){let n=Date.now();for(let[r,i]of t.pluginClients)n-i.lastSeen>3e4&&(t.pluginClients.delete(r),t.pendingCommands.delete(r));return AN(t),{serverInstanceId:t.instanceId,sessionId:t.sessionId,mcpVersion:We,uptime:n-t.startTime,serverStartTime:t.startTime,serverExecutable:process.execPath,serverHost:t.config.httpHost,serverPort:t.config.httpPort,serverPid:process.pid,mcpInstances:[{instanceId:t.instanceId,pid:process.pid,connectedAt:t.startTime,isServer:!0,cwd:process.cwd(),projectRoot:ml(),...t.aiClientName?{aiClientName:t.aiClientName}:{}},...Array.from(t.mcpInstances.values())],mcpInstanceCount:t.mcpInstances.size+1}}function BN(t,e){e.json(qN(t))}function ZN(t,e,n){let r=e.query.instanceId;r&&t.mcpInstances.has(r)&&(t.mcpInstances.get(r).lastSeen=Date.now()),AN(t);let i=LQ(t),a=t.sseClients.size,c={...{status:"online",connectedClients:a+i.length,queuedCommands:t.commandQueue.size,uptime:Date.now()-t.startTime,version:We,enableContextCapture:t.executionContextManager?.isEnabled()??t.config.enableContextCapture??!0,isClientMode:t.isClientMode,pid:process.pid,sessionId:t.sessionId},instanceId:t.instanceId,mcpInstanceCount:t.mcpInstances.size+1,aiClientNames:zN(t),pluginVersion:t.pluginVersion||void 0,sseClients:a,dashboardSseClients:t.dashboardSseClients?.size??0,pollingClients:i.length,pluginClients:i.map(l=>({clientId:l.clientId,projectName:l.projectName,placeName:l.placeName,pluginVersion:l.pluginVersion,lastSeen:Date.now()-l.lastSeen})),...t.isClientMode?{upstream:{reachable:t.clientModeUpstreamReachable,consecutiveFailures:t.clientModeConsecutiveHealthFailures,lastSuccessAt:t.clientModeLastHealthSuccessAt,lastFailureAt:t.clientModeLastHealthFailureAt,lastError:t.clientModeLastHealthError,baseUrl:t.baseUrl}}:{}};n.json(c)}function HN(t,e,n,r){let i=e.ip||e.socket.remoteAddress||"";if(!(i==="127.0.0.1"||i==="::1"||i==="::ffff:127.0.0.1"||i==="localhost")){y.warn("Shutdown request rejected from non-localhost",{ip:i}),n.status(403).json({error:"Forbidden: localhost only"});return}y.info("Shutdown request received, initiating graceful shutdown",{requestedBy:i,uptime:Date.now()-t.startTime}),n.json({status:"shutting_down",message:"Server will shutdown gracefully",pid:process.pid}),setTimeout(async()=>{try{await r(),y.info("Graceful shutdown completed"),process.exit(0)}catch(o){y.error("Error during graceful shutdown",o),process.exit(1)}},100)}async function jf(t){if(t.isClientMode)try{let e=await fetch(`${t.baseUrl}/connection-info`);if(e.ok)return await e.json()}catch(e){y.warn("Failed to fetch connection info from server",{error:e})}return qN(t)}pe();var zf={query_instances:{discriminator:"action",mapping:{get:"get_instance",children:"get_instance_children",find_child:"find_first_child",find_descendant:"find_first_descendant",wait_for_child:"wait_for_child",class_info:"get_class_info",search_name:"search_by_name",search_class:"search_by_class",search_property:"search_by_property",search_tag:"search_by_tag",file_tree:"get_file_tree",project_structure:"get_project_structure",descendants:"get_descendants",ancestors:"get_ancestors"},paramAliases:{search_by_name:{query:"pattern"},search_by_property:{root:"rootPath"},search_by_tag:{root:"rootPath"},get_project_structure:{root:"rootPath"}}},mutate_instances:{discriminator:"action",mapping:{create:"create_instance",create_with_props:"create_instance_with_properties",delete:"delete_instance",clone:"clone_instance",move:"move_instance",rename:"rename_instance",pivot:"pivot_to",create_tree:"create_instance_tree",mass_create:"mass_create_instances",mass_delete:"mass_delete_instances",mass_duplicate:"mass_duplicate",smart_duplicate:"smart_duplicate"},paramAliases:{clone_instance:{path:"sourcePath"}}},manage_properties:{discriminator:"action",mapping:{get:"get_property",set:"set_property",get_all:"get_all_properties",set_multiple:"set_multiple_properties",get_attr:"get_attribute",set_attr:"set_attribute",get_all_attrs:"get_all_attributes",delete_attr:"delete_attribute",add_tag:"add_tag",remove_tag:"remove_tag",check_tag:"has_tag",get_tags:"get_tags",get_tagged:"get_tagged",set_calculated:"set_calculated_property",set_relative:"set_relative_property",mass_set:"mass_set_property",mass_get:"mass_get_property",modify_children:"modify_children"},paramAliases:{get_tagged:{tagName:"tag",root:"rootPath"},set_relative_property:{amount:"value"}}},manage_scripts:{discriminator:"action",mapping:{get_source:"get_script_source",set_source:"set_script_source",create:"create_script",delete:"delete_script",edit_replace:"edit_script_lines",edit_insert:"insert_script_lines",edit_delete:"delete_script_lines",search:"search_in_scripts",replace:"replace_in_scripts",get_dependencies:"get_script_dependencies"},paramAliases:{edit_script_lines:{newLines:"newContent"},insert_script_lines:{lines:"content"},replace_in_scripts:{pattern:"searchPattern"}}},manage_lighting:{discriminator:"action",mapping:{lighting:"set_lighting",atmosphere:"set_atmosphere",sky:"set_sky",terrain_props:"set_terrain",time:"set_time_of_day"}},manage_selection:{discriminator:"action",mapping:{get:"get_selection",set:"set_selection",clear:"clear_selection",cached:"get_cached_selection",context:"get_selection_context",details:"get_selection_details",add:"add_to_selection",remove:"remove_from_selection",watch:"watch_selection"}},manage_camera:{discriminator:"action",mapping:{info:"get_camera_info",focus_path:"focus_camera_path",focus_position:"focus_camera_position",suggest:"get_suggested_camera_view"},paramAliases:{get_suggested_camera_view:{path:"targetPath"}}},manage_tween:{discriminator:"action",mapping:{create:"create_tween",play:"play_tween",pause:"pause_tween",cancel:"cancel_tween"}},manage_audio:{discriminator:"action",mapping:{play:"play_sound",stop:"stop_sound",pause:"pause_sound",resume:"resume_sound",set_listener:"set_listener"}},manage_animation:{discriminator:"action",mapping:{load:"load_animation",play:"play_animation",stop:"stop_animation",get_tracks:"get_animation_tracks"}},manage_physics:{discriminator:"action",mapping:{register_group:"register_collision_group",set_collidable:"set_collidable",get_groups:"get_collision_groups"}},manage_effects:{discriminator:"action",mapping:{emit:"emit_particles",clear:"clear_particles",toggle:"toggle_effect"}},manage_terrain:{discriminator:"action",mapping:{fill_block:"terrain_fill_block",fill_ball:"terrain_fill_ball",fill_cylinder:"terrain_fill_cylinder",fill_wedge:"terrain_fill_wedge",clear_region:"terrain_clear",clear_bounds:"terrain_clear_region",replace_material:"terrain_replace_material",colors_get:"terrain_get_material_color",colors_set:"terrain_set_material_color",read_voxel:"terrain_read_voxel",read_voxels:"terrain_read_voxels",write_voxels:"terrain_write_voxels",generate:"terrain_generate",smooth:"terrain_smooth"}},spatial_query:{discriminator:"action",mapping:{raycast:"raycast",find_ground:"find_ground",check_placement:"check_placement",multi_raycast:"multi_raycast",scan_area:"scan_area",find_flat:"find_flat_areas",find_spawn:"find_spawn_positions",analyze_walkable:"analyze_walkable_area",spatial_map:"get_spatial_map",find_space:"find_empty_space",bounds:"get_bounds",snap_grid:"snap_to_grid",collision:"check_collision"},paramAliases:{get_spatial_map:{path:"rootPath"}}},manage_assets:{discriminator:"action",mapping:{insert:"insert_model",info:"get_asset_info",search:"search_creator_store",search_insert:"search_and_insert_model",insert_free:"insert_free_model",insert_package:"insert_package",export:"export_selection"},paramAliases:{search_creator_store:{maxResults:"limit"}}},manage_sync:{discriminator:"action",mapping:{status:"sync_status",config:"sync_config",history:"sync_history",directions:"sync_directions",read_file:"sync_read_file",write_file:"sync_write_file",progress:"sync_progress"}},workspace_state:{discriminator:"action",mapping:{sync:"sync_workspace_state",snapshot:"get_workspace_snapshot",changes:"get_recent_changes",viewport:"get_viewport_info",clear_history:"clear_change_history",metadata:"get_workspace_metadata",scripts:"get_script_list",selection_info:"get_selection_info",clear_cache:"clear_state_cache"}},manage_logs:{discriminator:"action",mapping:{get:"get_output_logs",clear:"clear_output_logs",errors:"get_recent_errors"},paramAliases:{get_output_logs:{level:"type"}}},system_info:{discriminator:"action",mapping:{ping:"ping",connection:"get_connection_info",usage:"get_usage_status",place_info:"get_place_info",services:"get_services",studio_settings:"get_studio_settings",play:"start_playtest",stop:"stop_playtest",pause:"pause_playtest",resume:"resume_playtest",play_status:"get_play_status",run_test:"run_test"}}};var UQ={get_cached_selection:"internal",sync_status:"internal",sync_config:"internal",sync_history:"internal",sync_directions:"internal",sync_read_file:"internal",sync_write_file:"internal",sync_progress:"internal",get_connection_info:"internal",run_test:"internal"};function Af(t){return UQ[t]||"plugin"}var Nf=100,FQ=Object.entries(zf).reduce((t,[e,n])=>{for(let r of Object.values(n.mapping))t[r]=e;return t},{});function VN(t){return{toolName:FQ[t]||t,actionName:t}}function WN(t,e){if(typeof t.contextId=="string"||!e||typeof e!="object")return t;let n=e.contextId;return typeof n=="string"?{...t,contextId:n}:t}function GN(t,e,n){y.info("Plugin connected via SSE"),n.setHeader("Content-Type","text/event-stream"),n.setHeader("Cache-Control","no-cache"),n.setHeader("Connection","keep-alive"),t.sseClients.add(n),oS(n,{event:"command",id:kl(),data:{action:"connected",requestId:kl(),params:{serverVersion:We,timestamp:Date.now()}}});let r=setInterval(()=>{oS(n,{event:"command",id:kl(),data:{action:"keepalive",requestId:kl(),params:{timestamp:Date.now()}}})},3e4);n.on("close",()=>{y.info("Plugin disconnected from SSE"),clearInterval(r),t.sseClients.delete(n)})}function oS(t,e){let n=JSON.stringify(e.data);t.write(`event: ${e.event}
|
|
126
|
+
`),this.clearTTLTimerForPlace(i,a),i.index.clearAllHashes(),i.index.clearClassMappings(),i.tmpIndex){let m=i.tmpIndex.getExplorerRoot(),h=i.index.getExplorerRoot();for(let[g,x]of i.tmpIndex.getAllHashes()){let w=$e.relative(m,g),_=$e.resolve(h,w);i.index.updateHashByValue(_,x)}for(let[g,x]of i.tmpIndex.getAllFileHashes()){let w=$e.relative(m,g),_=$e.resolve(h,w);i.index.updateFileHashByValue(_,x)}i.index.resetNameCounters(),i.index.mergeNameMappingsFrom(i.tmpIndex)}if(i.tmpIndex=null,i.tmpWriter&&(i.tmpWriter.stopChangeLogFlusher(),i.tmpWriter=null),this.pendingServiceTrees.delete(a),i.collisionDirMap=null,await i.index.saveToDisk(),i.state="syncing",i.activeFullSyncSessionId=null,i.syncProgress=null,this.ctx.touchRuntimePlace(e),this.ctx.activeFullSyncPlaceId=null,await this.ctx.startFileWatcherForPlace(i),i.fileWatcher&&await i.fileWatcher.waitUntilReady(),u.size>0){for(let[m,h]of u){let g=$e.resolve(o,m);try{await Xt.mkdir($e.dirname(g),{recursive:!0}),await Xt.writeFile(g,h,"utf-8")}catch(x){y.warn("Failed to restore preserved file",{path:m,error:x instanceof Error?x.message:String(x)})}}y.info("Restored preserved local files",{placeId:e,count:u.size})}if(p.length>0){let m=0;for(let h of p){let g=$e.resolve(o,h);try{await Xt.unlink(g),i.index.removeHash(g),m++}catch(x){x.code!=="ENOENT"&&y.warn("Failed to delete preserved-as-deleted file",{path:h,error:x instanceof Error?x.message:String(x)})}}m>0&&(await i.index.saveToDisk(),y.info("Deleted locally-removed files from new sync",{placeId:e,count:m}))}i.writer.appendChangeLog(`FULL_SYNC_COMPLETE instances=${i.instanceCount} scripts=${i.scriptCount}`),i.writer.appendHistory({timestamp:new Date().toISOString(),type:"fullSyncComplete",direction:"forward",path:`place_${e}`,details:`instances:${i.instanceCount} scripts:${i.scriptCount}`}),y.info("Full sync completed",{placeId:e,instanceCount:i.instanceCount,scriptCount:i.scriptCount}),r.status(200).json({status:"completed",instanceCount:i.instanceCount,scriptCount:i.scriptCount,syncRoot:this.ctx.config.getPlaceRoot(e)})}setPreserveLocalFiles(e,n){this.preserveLocalFilesMap.set(e,n)}getAndClearPreserveLocalFiles(e){let n=this.preserveLocalFilesMap.get(e)||[];return this.preserveLocalFilesMap.delete(e),n}clearPreserveLocalFiles(e){this.preserveLocalFilesMap.delete(e)}clearPendingServiceTrees(e){this.pendingServiceTrees.delete(e)}startTTLTimerForPlace(e,n){let r=setTimeout(async()=>{y.warn("Incomplete sync TTL expired, cleaning up",{placeId:e.placeId,syncId:n});let i=this.ctx.config.getPlaceRoot(e.placeId),a=$e.join(i,`explorer_tmp_${n}`);try{await Xt.rm(a,{recursive:!0,force:!0})}catch(o){y.error("Failed to clean up expired temp dir",o instanceof Error?o:new Error(String(o)))}e.incompleteSyncTimer=null,e.activeFullSyncSessionId===n&&(e.activeFullSyncSessionId=null,e.activeClientId=null,e.state="idle",e.tmpIndex=null,this.pendingServiceTrees.delete(n),e.collisionDirMap=null,e.tmpWriter&&(e.tmpWriter.stopChangeLogFlusher(),e.tmpWriter=null),this.ctx.activeFullSyncPlaceId===e.placeId&&(this.ctx.activeFullSyncPlaceId=null),this.ctx.clearRuntimePlaceIfMatch(e.placeId))},vN);r&&typeof r=="object"&&"unref"in r&&r.unref(),e.incompleteSyncTimer=r}getOrCreatePendingServiceTree(e,n){let r=this.pendingServiceTrees.get(e);r||(r=new Map,this.pendingServiceTrees.set(e,r));let i=r.get(n.serviceName);if(i)return i;let a={serviceName:n.tree?.name??n.serviceName,serviceClassName:n.tree?.className??n.serviceClassName,zeroBasedChunkIndex:n.chunkIndex===0,instances:[]};return r.set(n.serviceName,a),a}isLastChunk(e,n,r){return r<=1?!0:e.zeroBasedChunkIndex?n>=r-1:n>=r}buildServiceTree(e,n){let r={name:n.serviceName,className:n.serviceClassName,childCount:0,children:[],syncedAt:new Date().toISOString()};for(let i of n.instances){let a=this.resolveEffectiveSegments(e,i.effectivePath),o=gt(i.originalPath);this.upsertTreeNode(r,a,o,i.className)}return this.recomputeTreeChildCounts(r),r.syncedAt=new Date().toISOString(),r}resolveEffectiveSegments(e,n){return e.tmpIndex?gt(n).map(r=>e.tmpIndex.sanitizeName(r)):gt(n)}rewritePendingEffectivePaths(e,n,r,i){let a=$e.relative(n,r).split($e.sep).filter(l=>l.length>0),o=$e.relative(n,i).split($e.sep).filter(l=>l.length>0);if(a.length===0||o.length===0)return;let s=Xe(["game",...a]),c=Xe(["game",...o]);for(let l of e.instances){if(l.effectivePath===s){l.effectivePath=c;continue}(l.effectivePath.startsWith(`${s}.`)||l.effectivePath.startsWith(`${s}[`))&&(l.effectivePath=`${c}${l.effectivePath.slice(s.length)}`)}}upsertTreeNode(e,n,r,i){if(n.length<=1)return;let a=e.children;for(let o=1;o<n.length;o++){let s=n[o],c=r[o],l=o===n.length-1,u=a.find(p=>p.name===s);u?l&&(u.className=i,c!==void 0&&c!==s&&(u.originalName=c)):(u={name:s,className:l?i:"Folder",childCount:0,children:[]},c!==void 0&&c!==s&&(u.originalName=c),a.push(u)),u.children||(u.children=[]),a=u.children}}recomputeTreeChildCounts(e){let n=r=>{let i=r.children??[];r.children=i;for(let a of i)n(a);r.childCount=i.length};for(let r of e.children)n(r);e.childCount=e.children.length}clearTTLTimerForPlace(e,n){e.incompleteSyncTimer&&e.activeFullSyncSessionId===n&&(clearTimeout(e.incompleteSyncTimer),e.incompleteSyncTimer=null)}async cleanupStaleTempDirs(){let e=this.ctx.config.getSyncRoot();try{let n=await Xt.readdir(e,{withFileTypes:!0});for(let r of n)if(r.isDirectory()){if(r.name.startsWith("explorer_tmp_")){let i=$e.join(e,r.name);y.warn("Removing stale temp directory from crashed sync",{dir:r.name});try{await Xt.rm(i,{recursive:!0,force:!0})}catch(a){y.error(`Failed to remove stale temp dir: ${r.name}`,a instanceof Error?a:new Error(String(a)))}}if(r.name.startsWith("place_")){let i=$e.join(e,r.name);try{let a=await Xt.readdir(i,{withFileTypes:!0});for(let o of a)if(o.isDirectory()&&o.name.startsWith("explorer_tmp_")){let s=$e.join(i,o.name);y.warn("Removing stale temp directory from crashed sync",{dir:`${r.name}/${o.name}`});try{await Xt.rm(s,{recursive:!0,force:!0})}catch(c){y.error(`Failed to remove stale temp dir: ${r.name}/${o.name}`,c instanceof Error?c:new Error(String(c)))}}}catch{continue}}}}catch(n){if(n.code==="ENOENT")return;y.warn("Failed to scan for stale temp dirs",{error:n instanceof Error?n.message:String(n)})}}};import Gn from"path";import{promises as kN}from"fs";pe();var Cf=class{constructor(e){this.ctx=e}async handleReversePending(e,n){let r=this.ctx.resolveQueryPlaceId(e);if(r==null){n.status(200).json({pending:0,hasConflicts:!1,lastDetected:null});return}let i=this.ctx.places.get(r);if(!i){n.status(404).json({error:"Place not found",message:`No sync context for place ${r}`});return}let a={pending:i.fileWatcher?.getPendingCount()??0,hasConflicts:!1,lastDetected:i.fileWatcher?.getLastDetected()??null,forwardRestoreNeeded:i.forwardRestoreQueue.length};this.ctx.touchRuntimePlace(r),n.status(200).json(a)}async handleReverseSyncChanges(e,n){let r=this.ctx.resolveQueryPlaceId(e);if(r==null){n.status(200).json({changes:[],count:0});return}let i=this.ctx.places.get(r);if(!i){n.status(404).json({error:"Place not found",message:`No sync context for place ${r}`});return}if(i.state!=="syncing"){n.status(400).json({error:"Not syncing",message:"Reverse sync is only available when sync is active"});return}let a=i.fileWatcher?.drainPendingChanges()??[],o=a.length>0?await i.reader.buildChangesFromPending(a):[];this.ctx.touchRuntimePlace(r),n.status(200).json({changes:o,count:o.length})}async handleReverseSyncResult(e,n){let r=e.body,i=r.placeId??this.ctx.getDefaultRuntimePlaceId();if(i==null){n.status(400).json({error:"Validation error",message:"placeId is required (in body or via active sync session)"});return}let a=r.appliedFiles??r.appliedPaths;if(!a||!Array.isArray(a)){n.status(400).json({error:"Validation error",message:"appliedFiles (or appliedPaths) must be an array of relative file paths"});return}let o=this.ctx.places.get(i);if(!o){n.status(404).json({error:"Place not found",message:`No sync context for place ${i}`});return}this.ctx.touchRuntimePlace(i);let s=this.ctx.config.getPlaceRoot(i),c=0,l=[];for(let u of a){let p=Gn.resolve(s,u);if(!kf(s,p)){l.push({path:u,error:"Path is outside the place root"});continue}try{let d=await kN.readFile(p,"utf-8"),f=o.index.computeHash(d);o.index.updateHashByValue(p,f),o.index.updateFileHashByValue(p,f),c++}catch(d){let f=d.code;if(f==="ENOENT"){o.index.removeHash(p),o.index.removeHashesUnder(p);let m=this.resolveInstancePathForAppliedPath(o.index,p);if(m){let h=Ct(m),g=Et(m);g&&h&&await o.writer.removeFromTree(g,h)}c++}else f==="EISDIR"?c++:l.push({path:u,error:d instanceof Error?d.message:String(d)})}}c>0&&await o.index.saveToDisk(),c>0&&o.writer.appendHistory({timestamp:new Date().toISOString(),type:"reverseApply",direction:"reverse",path:`place_${i}`,details:`applied:${c} failed:${l.length}`}),n.status(200).json({updated:c,failed:l.length,errors:l})}async handleResolveConflict(e,n){let r=e.body;if(!r.fsPath||!r.resolution){n.status(400).json({error:"Validation error",message:"fsPath and resolution are required"});return}let{fsPath:i,resolution:a}=r,o=this.ctx.config.getSyncRoot();if(!kf(o,Gn.resolve(o,i))){n.status(403).json({error:"Forbidden",message:"Path is outside the sync root"});return}if(a==="skip"){n.status(200).json({status:"skipped",fsPath:i});return}let s;if(r.placeId&&(s=this.ctx.places.get(r.placeId)),!s){let d=i.match(/^place_(\d+)(?:_[^/]+)?\//);if(d){let f=parseInt(d[1],10);s=this.ctx.places.get(f)}else s=Array.from(this.ctx.places.values())[0]}if(!s){n.status(404).json({error:"No active place context",message:"No sync session is active. Start a sync first."});return}let c=this.ctx.config.getPlaceRoot(s.placeId),l=Gn.resolve(c,i);if(!kf(c,l)){n.status(403).json({error:"Forbidden",message:"Path is outside the place root"});return}let u=s.index,p=s.reader;if(a==="apply-studio"){u.resolveFile(l,"apply-studio"),await u.saveToDisk(),n.status(200).json({status:"resolved",resolution:"apply-studio",fsPath:i});return}if(a==="apply-file"){let d=await kN.readFile(l,"utf-8"),f=u.computeHash(d);u.resolveFile(l,"apply-file",f),await u.saveToDisk();let m=p.getFileType(l),h=p.resolveInstancePathFromFile(l);n.status(200).json({status:"resolved",resolution:"apply-file",fsPath:i,instancePath:h,fileType:m,content:d});return}n.status(400).json({error:"Invalid resolution",message:`Unknown resolution: ${a}`})}async handleReverseRescan(e,n){let r=this.ctx.resolveQueryPlaceId(e);if(r==null){n.status(200).json({added:0});return}let i=this.ctx.places.get(r);if(!i){n.status(404).json({error:"Place not found",message:`No sync context for place ${r}`});return}if(!i.fileWatcher){n.status(200).json({added:0});return}let a=await i.fileWatcher.rescan();this.ctx.touchRuntimePlace(r),y.info("Reverse rescan completed",{placeId:r,added:a}),n.status(200).json({added:a})}resolveInstancePathForAppliedPath(e,n){let r=e.resolveInstancePathFromFsPath(n);if(r)return r;let i=e.getExplorerRoot(),a=Gn.relative(i,n);if(a.startsWith("..")||a===""||Gn.isAbsolute(a))return null;let o=a.split(Gn.sep).filter(f=>f.length>0);if(o.length<2)return null;let s=Gn.basename(n),c=Gn.dirname(n),l=e.getOriginalInstance(c,s);if(l)return l.instancePath;let u=s.toLowerCase();if(Co.some(f=>u.endsWith(f))||u==="_tree.json")return null;let p=["game"],d=i;for(let f of o){d=Gn.join(d,f);let m=Gn.dirname(d);p.push(e.getOriginalNameForDir(m,f))}return Xe(p)}};pe();function IN(t){if(!t||typeof t!="object")return;let e=t;if(Array.isArray(e.instances))for(let n of e.instances)Array.isArray(n.properties)&&n.properties.length===0&&(n.properties={}),Array.isArray(n.attributes)&&n.attributes.length===0&&(n.attributes={});if(Array.isArray(e.changes))for(let n of e.changes)Array.isArray(n.properties)&&n.properties.length===0&&(n.properties={}),Array.isArray(n.attributes)&&n.attributes.length===0&&(n.attributes={})}var Uo=class{config;places;apiHandler;changeProcessor;initHandler;reverseHandler;activeFullSyncPlaceId=null;activeRuntimeSyncPlaceId=null;constructor(e){this.config=new cf(e),this.apiHandler=new Pf(this),this.changeProcessor=new If(this),this.initHandler=new $f(this),this.reverseHandler=new Cf(this),this.places=new sf({max:3,dispose:(n,r)=>{y.info("Disposing place context (LRU eviction)",{placeId:r}),this.activeFullSyncPlaceId===r&&(this.activeFullSyncPlaceId=null),this.activeRuntimeSyncPlaceId===r&&(this.activeRuntimeSyncPlaceId=null),n.fileWatcher&&(n.fileWatcher.stop().catch(i=>{y.error("Error stopping file watcher during dispose",i)}),n.fileWatcher=null),n.writer.stopChangeLogFlusher(),n.incompleteSyncTimer&&(clearTimeout(n.incompleteSyncTimer),n.incompleteSyncTimer=null),n.index.saveToDisk().catch(i=>{y.error("Error saving index during dispose",i)}),n.activeFullSyncSessionId&&this.initHandler.clearPendingServiceTrees(n.activeFullSyncSessionId),n.tmpWriter&&(n.tmpWriter.stopChangeLogFlusher(),n.tmpWriter=null),n.tmpIndex=null,n.collisionDirMap=null}})}getSyncRoot(){return this.config.getSyncRoot()}async getOrCreatePlaceContext(e,n){let r=this.places.get(e);if(r&&n){let i=this.config.getPlaceRoot(e),a=await this.config.resolvePlaceRoot(e,n);a!==i&&(y.info("Place root migrated, recreating context",{placeId:e,from:i,to:a}),this.places.delete(e),r=void 0)}if(!r){let i=await this.config.resolvePlaceRoot(e,n),a=Yw.join(i,"explorer");await Lo.mkdir(i,{recursive:!0}),await Lo.mkdir(a,{recursive:!0});let o=new Yr(i,a);await o.loadFromDisk();let s=new jo(this.config,o,e),c=new pf(this.config,o,i);s.startChangeLogFlusher(),r={placeId:e,placeName:"",index:o,writer:s,reader:c,fileWatcher:null,state:"idle",activeClientId:null,activeFullSyncSessionId:null,instanceCount:0,scriptCount:0,lastFullSync:null,lastIncrementalSync:null,tmpIndex:null,tmpWriter:null,incompleteSyncTimer:null,changesSinceLastSave:0,directions:{...qi},applyModes:{...Bi},forwardRestoreQueue:[],syncProgress:null,collisionDirMap:null},this.places.set(e,r),y.info("Created new place context",{placeId:e,placeRoot:i})}return r}async handleSyncInit(e,n){try{IN(e.body);let r=bN(e.body);if(!r.success){n.status(400).json({error:"Validation error",message:r.error});return}let i=r.data,a=i.phase==="start"?i.placeId??null:i.phase==="chunk"||i.phase==="complete"?this.activeFullSyncPlaceId:null;if(a==null){n.status(400).json({error:"Missing placeId",message:"placeId is required in start phase or must be set by previous start"});return}switch(i.phase){case"start":await this.initHandler.handleInitStart(a,i,n);break;case"chunk":await this.initHandler.handleInitChunk(a,i,n);break;case"complete":await this.initHandler.handleInitComplete(a,i,n);break}}catch(r){this.sendError(n,r,"handleSyncInit")}}async handleSyncUpdate(e,n){try{IN(e.body);let r=_N(e.body);if(!r.success){n.status(400).json({error:"Validation error",message:r.error});return}let i=r.data,a=i.placeId??this.getDefaultRuntimePlaceId();if(a==null){n.status(400).json({error:"Missing placeId",message:"placeId is required in body or must have an active sync session"});return}let o=await this.getOrCreatePlaceContext(a);if(o.activeFullSyncSessionId!==null||o.state==="initializing"){n.status(409).json({error:"Conflict",message:`Full sync in progress for place ${a}`});return}o.activeClientId=i.clientId,this.touchRuntimePlace(a);let s=yN(o,i.changes);y.info("Sync update received",{placeId:a,clientId:i.clientId,changeCount:s.length,receivedCount:i.changes.length,types:s.map(f=>f.type)});let c=[],l=[],u=0,p=new Map;for(let f of s)try{let m=await this.changeProcessor.processChangeForPlace(o,f,p);m?l.push(m):u++}catch(m){let h="path"in f?f.path:"oldPath"in f?f.oldPath:"unknown";c.push({path:h,error:m instanceof Error?m.message:String(m)})}for(let f of p.values())try{await _f(o,f)}catch(m){c.push({path:f.instancePath,error:m instanceof Error?m.message:String(m)})}o.changesSinceLastSave+=u,o.changesSinceLastSave>=xN&&(await o.index.saveToDisk(),o.changesSinceLastSave=0),o.lastIncrementalSync=new Date().toISOString();let d={processed:u,failed:c.length,errors:c,syncedAt:o.lastIncrementalSync};l.length>0&&(d.conflicts=l),n.status(200).json(d)}catch(r){this.sendError(n,r,"handleSyncUpdate")}}getDefaultRuntimePlaceId(){if(this.activeRuntimeSyncPlaceId!==null&&this.activeRuntimeSyncPlaceId!==void 0){if(this.places.has(this.activeRuntimeSyncPlaceId))return this.activeRuntimeSyncPlaceId;this.activeRuntimeSyncPlaceId=null}if(this.activeFullSyncPlaceId!==null&&this.activeFullSyncPlaceId!==void 0&&this.places.has(this.activeFullSyncPlaceId))return this.activeRuntimeSyncPlaceId=this.activeFullSyncPlaceId,this.activeRuntimeSyncPlaceId;for(let[e,n]of this.places.entries())if(n.state==="syncing"||n.state==="initializing")return this.activeRuntimeSyncPlaceId=e,e;return this.activeRuntimeSyncPlaceId=null,null}touchRuntimePlace(e){this.activeRuntimeSyncPlaceId=e}clearRuntimePlaceIfMatch(e){this.activeRuntimeSyncPlaceId===e&&(this.activeRuntimeSyncPlaceId=null)}resolveQueryPlaceId(e,n="runtime"){let r=e.query.placeId;if(r){let i=parseInt(r,10);if(!isNaN(i))return i}return n==="full"?this.activeFullSyncPlaceId:this.getDefaultRuntimePlaceId()}async handleSyncStatus(e,n){try{let r=this.resolveQueryPlaceId(e);if(r==null){let s={state:"idle",instanceCount:0,scriptCount:0,lastFullSync:null,lastIncrementalSync:null,syncRoot:this.config.getSyncRoot(),activeClientId:null,reverseSyncAvailable:!1,modifiedFileCount:0};n.status(200).json(s);return}let i=this.places.get(r);if(!i){let s={state:"idle",instanceCount:0,scriptCount:0,lastFullSync:null,lastIncrementalSync:null,syncRoot:this.config.getPlaceRoot(r),activeClientId:null,reverseSyncAvailable:!1,modifiedFileCount:0};n.status(200).json(s);return}let a=i.fileWatcher?.getPendingCount()??0;this.touchRuntimePlace(r);let o={state:i.state,instanceCount:i.instanceCount,scriptCount:i.scriptCount,lastFullSync:i.lastFullSync,lastIncrementalSync:i.lastIncrementalSync,syncRoot:this.config.getPlaceRoot(r),activeClientId:i.activeClientId,reverseSyncAvailable:a>0,modifiedFileCount:a,applyModes:i.applyModes,directions:i.directions,fileWatcherActive:i.fileWatcher!==null,forwardOnlyClasses:[...To]};n.status(200).json(o)}catch(r){this.sendError(n,r,"handleSyncStatus")}}async handleSyncStop(e,n){try{let r=e.body,i=r.placeId??this.getDefaultRuntimePlaceId();if(i==null){n.status(200).json({status:"idle",state:"idle",placeId:null,message:"No active sync place"});return}let a=this.places.get(i);if(!a){n.status(200).json({status:"idle",state:"idle",placeId:i,message:`No sync context for place ${i}`});return}if(r.clientId&&a.activeClientId&&r.clientId!==a.activeClientId&&(a.activeFullSyncSessionId!==null||a.state==="syncing"||a.state==="initializing")){n.status(409).json({error:"Conflict",message:`This sync session belongs to client ${a.activeClientId}`});return}let o=a.activeFullSyncSessionId;if(o&&(this.initHandler.clearPreserveLocalFiles(o),this.initHandler.clearPendingServiceTrees(o)),a.fileWatcher&&(await a.fileWatcher.stop(),a.fileWatcher=null),a.incompleteSyncTimer&&(clearTimeout(a.incompleteSyncTimer),a.incompleteSyncTimer=null),o){let s=this.config.getPlaceRoot(i),c=Yw.join(s,`explorer_tmp_${o}`);await Lo.rm(c,{recursive:!0,force:!0}).catch(()=>{})}a.tmpWriter&&(a.tmpWriter.stopChangeLogFlusher(),a.tmpWriter=null),a.tmpIndex=null,a.collisionDirMap=null,a.activeFullSyncSessionId=null,a.syncProgress=null,a.state="idle",a.activeClientId=null,a.instanceCount=0,a.scriptCount=0,this.activeFullSyncPlaceId===i&&(this.activeFullSyncPlaceId=null),this.clearRuntimePlaceIfMatch(i),y.info("Sync stopped",{placeId:i,reason:r.reason??"requested"}),n.status(200).json({status:"stopped",state:"idle",placeId:i})}catch(r){this.sendError(n,r,"handleSyncStop")}}async handleSyncConfig(e,n){try{if(e.method==="GET"){n.status(200).json(this.config.getConfig());return}let r=wN(e.body);if(!r.success){n.status(400).json({error:"Validation error",message:r.error});return}let i=r.data;i.maxDepth!==void 0&&this.config.updateConfig({maxDepth:i.maxDepth}),i.maxInstances!==void 0&&this.config.updateConfig({maxInstances:i.maxInstances}),n.status(200).json({status:"updated",config:this.config.getConfig()})}catch(r){this.sendError(n,r,"handleSyncConfig")}}async initialize(){try{await this.config.loadFromMeta(),await this.initHandler.cleanupStaleTempDirs(),y.info("SyncController initialized")}catch(e){y.error("SyncController initialization failed",e instanceof Error?e:new Error(String(e)))}}async shutdown(){try{this.places.clear(),y.info("SyncController shut down")}catch(e){y.error("SyncController shutdown error",e instanceof Error?e:new Error(String(e)))}}async atomicWriteFile(e,n){let r=e+".tmp."+NQ().slice(0,8);try{await Lo.writeFile(r,n,"utf-8"),await Lo.rename(r,e)}catch(i){throw await Lo.unlink(r).catch(()=>{}),i}}async handlePreCheck(e,n){try{await this.apiHandler.handlePreCheck(e,n)}catch(r){this.sendError(n,r,"handlePreCheck")}}async handleSyncDirections(e,n){try{await this.apiHandler.handleSyncDirections(e,n)}catch(r){this.sendError(n,r,"handleSyncDirections")}}async handleForwardRestoreList(e,n){try{await this.apiHandler.handleForwardRestoreList(e,n)}catch(r){this.sendError(n,r,"handleForwardRestoreList")}}async handleReversePending(e,n){try{await this.reverseHandler.handleReversePending(e,n)}catch(r){this.sendError(n,r,"handleReversePending")}}async handleReverseSyncChanges(e,n){try{await this.reverseHandler.handleReverseSyncChanges(e,n)}catch(r){this.sendError(n,r,"handleReverseSyncChanges")}}async handleReverseSyncResult(e,n){try{await this.reverseHandler.handleReverseSyncResult(e,n)}catch(r){this.sendError(n,r,"handleReverseSyncResult")}}async handleResolveConflict(e,n){try{await this.reverseHandler.handleResolveConflict(e,n)}catch(r){this.sendError(n,r,"handleResolveConflict")}}async handleReverseRescan(e,n){try{await this.reverseHandler.handleReverseRescan(e,n)}catch(r){this.sendError(n,r,"handleReverseRescan")}}async handleSyncHistory(e,n){try{await this.apiHandler.handleSyncHistory(e,n)}catch(r){this.sendError(n,r,"handleSyncHistory")}}async startFileWatcherForPlace(e){if(e.state!=="syncing"){y.debug("Skipping file watcher start - place not syncing",{placeId:e.placeId,state:e.state});return}e.fileWatcher&&await e.fileWatcher.stop();let n=Yw.join(this.config.getPlaceRoot(e.placeId),"explorer");e.fileWatcher=new bf(n,e.index),e.writer.setOnWriteCallback(r=>{e.fileWatcher?.suppressPath(r)}),e.fileWatcher.setDirectionChecker(r=>{let i=FA(r);return e.directions[i]}),e.fileWatcher.setOnForwardViolation(r=>{e.forwardRestoreQueue.includes(r)||(e.forwardRestoreQueue.push(r),y.info("Forward violation queued for restore",{placeId:e.placeId,relativePath:r,queueSize:e.forwardRestoreQueue.length}))}),await e.fileWatcher.start(),y.info("File watcher started for reverse sync",{placeId:e.placeId})}getStatusSummary(){return this.apiHandler.getStatusSummary()}getDirectionForCategory(e){return this.apiHandler.getDirectionForCategory(e)}getStatusDirect(e){return this.apiHandler.getStatusDirect(e)}getConfigDirect(){return this.apiHandler.getConfigDirect()}async getHistoryDirect(e,n){return this.apiHandler.getHistoryDirect(e,n)}getDirectionsDirect(e){return this.apiHandler.getDirectionsDirect(e)}getProgressDirect(e){return this.apiHandler.getProgressDirect(e)}async readSyncedFile(e,n){return this.apiHandler.readSyncedFile(e,n)}async writeSyncedFile(e,n,r){await this.apiHandler.writeSyncedFile(e,n,r)}async executeViaDisk(e,n){return this.apiHandler.executeViaDisk(e,n)}sendError(e,n,r){let i=n instanceof Error?n.message:String(n);if(i.includes("Path traversal detected")){e.status(403).json({error:"Forbidden",message:i});return}let a=n.code;if(a==="ENOSPC"||a==="EPERM"||a==="EACCES"){e.status(500).json({error:"Disk error",message:i});return}y.error(`SyncController.${r} failed`,n instanceof Error?n:new Error(i)),e.status(500).json({error:"Internal error",message:`${r}: ${i}`})}};import{randomUUID as OQ}from"crypto";function PN(t,e){let n=OQ(),r=n.replace(/-/g,"").substring(0,8).toUpperCase(),i=`${r.substring(0,4)}-${r.substring(4,8)}`;return{config:t,app:e,instanceId:n,sessionId:i,startTime:Date.now(),baseUrl:`http://${t.httpHost}:${t.httpPort}`,commandQueue:new Map,pendingCommands:new Map,globalPendingCommands:[],totalCommandsProcessed:0,pluginClients:new Map,mcpInstances:new Map,sseClients:new Set,cachedSelectionMap:new Map,isClientMode:!1,clientModeHealthTimer:null,clientModeConsecutiveHealthFailures:0,clientModeUpstreamReachable:!0,clientModeUpstreamContextCaptureEnabled:!0,clientModeLastHealthSuccessAt:null,clientModeLastHealthFailureAt:null,clientModeLastHealthError:null,historyManager:null,analyticsManager:null,executionContextManager:null,licenseState:null,syncController:null,internalActionExecutor:null,activeSyncOwnerInstanceId:null,activeProjectRoot:null,playtestControlCommand:null,aiClientName:"",pluginVersion:"",syncedSessionToken:null,serverLastCommandAt:null}}var $N=ri(xo(),1);pe();function CN(t){let e=$N.default.json({limit:"5mb"});t.app.use((n,r,i)=>{if(n.path.startsWith("/sync/")){i();return}e(n,r,i)}),t.app.use((n,r,i)=>{r.setHeader("Access-Control-Allow-Origin","http://localhost:3002"),r.setHeader("Access-Control-Allow-Methods","GET, POST, OPTIONS"),r.setHeader("Access-Control-Allow-Headers","Content-Type"),i()}),t.app.use((n,r,i)=>{y.debug(`${n.method} ${n.path}`,{ip:n.ip}),i()})}import{randomUUID as kl}from"crypto";pe();function DQ(){let t=process.env.WEPPY_ROBLOX_MCP_VERSION?.trim();return t||null}var We=DQ()??"2.0.10";Rf();function zN(t){let e=new Set;t.aiClientName&&e.add(t.aiClientName);for(let n of t.mcpInstances.values())n.aiClientName&&e.add(n.aiClientName);return Array.from(e)}function LQ(t){let n=Date.now();return Array.from(t.pluginClients.values()).filter(r=>n-r.lastSeen<1e4)}function AN(t){let n=Date.now();for(let[r,i]of t.mcpInstances)i.lastSeen&&n-i.lastSeen>15e3&&(t.mcpInstances.delete(r),i.sessionId&&t.executionContextManager?.endSession(i.sessionId),y.debug("Removed stale MCP instance",{instanceId:r,lastSeen:i.lastSeen}))}function NN(t,e,n){let r=e.query.clientId;if(r&&t.pluginClients.has(r)){let i=t.pluginClients.get(r);i.lastSeen=Date.now()}try{let i=e.body;if(!i||!Array.isArray(i.selection)||typeof i.count!="number"){y.warn("Invalid selection update request",{body:i}),n.status(400).json({error:"Invalid request body"});return}let a=r||"unknown",o=Date.now();t.cachedSelectionMap.set(a,{selection:i.selection,count:i.count,timestamp:o,clientId:a}),y.debug("Selection cache updated",{count:i.count,clientId:a,timestamp:o}),n.json({status:"ok",timestamp:o})}catch(i){y.error("Error handling selection update",i),n.status(500).json({error:"Internal server error"})}}function ON(t,e,n){let r=parseInt(e.query.maxAge)||3e4,i=Sl(t,r);i?n.json({cached:!0,...i}):n.json({cached:!1,message:"No cached selection available"})}function Sl(t,e=3e4,n){if(t.cachedSelectionMap.size===0)return null;let r;if(n)r=t.cachedSelectionMap.get(n);else for(let a of t.cachedSelectionMap.values())(!r||a.timestamp>r.timestamp)&&(r=a);if(!r)return null;let i=Date.now()-r.timestamp;return e===0||i<=e?r:null}function DN(t,e,n){try{let r=e.body;if(!r.clientId){n.status(400).json({error:"Missing clientId"});return}let i=t.pluginClients.get(r.clientId),a=Date.now(),o={clientId:r.clientId,projectName:r.projectName,placeName:r.placeName,placeId:r.placeId,pluginVersion:r.pluginVersion,connectedAt:i?.connectedAt||a,lastSeen:a,commandsProcessed:i?.commandsProcessed||0,connectionType:"polling"};t.pluginClients.set(r.clientId,o),t.pendingCommands.has(r.clientId)||t.pendingCommands.set(r.clientId,[]),r.pluginVersion&&(t.pluginVersion=r.pluginVersion),$t(t,"connection",{clientId:r.clientId,placeId:o.projectName,placeName:o.placeName,status:"connected"}),dl(t,{timestamp:new Date().toISOString(),type:"plugin",status:"connected",clientId:r.clientId,message:`Plugin connected \u2014 ${r.clientId}`,...o.placeId!==void 0?{placeId:o.placeId}:{},...o.placeName?{placeName:o.placeName}:{}}),t.analyticsManager&&(r.pluginVersion&&t.analyticsManager.setPluginVersion(r.pluginVersion),t.analyticsManager.trackPluginConnected()),y.info("Plugin client registered",{clientId:r.clientId,projectName:r.projectName,placeName:r.placeName,isReconnect:!!i}),n.json({status:"ok",clientId:r.clientId,serverInstanceId:t.instanceId,sessionId:t.sessionId,mcpVersion:We,connectedAt:o.connectedAt,aiClientNames:zN(t),serverStartTime:t.startTime,mcpInstanceCount:t.mcpInstances.size+1})}catch(r){y.error("Error registering plugin client",r),n.status(500).json({error:"Internal server error"})}}function MN(t,e,n){let r=e.body?.clientId;if(!r){n.status(400).json({error:"Missing clientId"});return}let i=t.pluginClients.has(r);t.pluginClients.delete(r),t.pendingCommands.delete(r),i&&($t(t,"connection",{clientId:r,status:"disconnected"}),dl(t,{timestamp:new Date().toISOString(),type:"plugin",status:"disconnected",clientId:r,message:`Plugin disconnected \u2014 ${r}`})),y.info("Plugin client unregistered",{clientId:r,existed:i}),n.json({status:"ok",existed:i})}function LN(t,e,n){try{let r=e.body;if(!r.instanceId){n.status(400).json({error:"Missing instanceId"});return}let i=Date.now(),a={instanceId:r.instanceId,...typeof r.sessionId=="string"?{sessionId:r.sessionId}:{},pid:r.pid,connectedAt:i,isServer:!1,lastSeen:i};r.aiClientName&&(a.aiClientName=r.aiClientName),r.cwd&&(a.cwd=r.cwd),"projectRoot"in r&&(a.projectRoot=r.projectRoot),t.mcpInstances.set(r.instanceId,a),$t(t,"mcp_status",{aiClientName:a.aiClientName??"Unknown",instanceId:r.instanceId,status:"registered"}),dl(t,{timestamp:new Date().toISOString(),type:"mcp",status:"registered",instanceId:r.instanceId,message:`MCP registered \u2014 ${a.aiClientName??r.instanceId}`,...a.aiClientName?{aiClientName:a.aiClientName}:{}}),y.info("MCP instance registered (client mode)",{instanceId:r.instanceId,pid:r.pid,cwd:r.cwd}),n.json({status:"ok",instanceId:r.instanceId,serverInstanceId:t.instanceId,sessionId:t.sessionId,mcpVersion:We,mcpInstanceCount:t.mcpInstances.size+1})}catch(r){y.error("Error registering MCP instance",r),n.status(500).json({error:"Internal server error"})}}function UN(t,e,n){let r=e.body?.instanceId;if(!r){n.status(400).json({error:"Missing instanceId"});return}let i=t.mcpInstances.get(r),a=!!i;t.mcpInstances.delete(r),i?.sessionId&&t.executionContextManager?.endSession(i.sessionId),a&&($t(t,"mcp_status",{aiClientName:i?.aiClientName??"Unknown",instanceId:r,status:"unregistered"}),dl(t,{timestamp:new Date().toISOString(),type:"mcp",status:"unregistered",instanceId:r,message:`MCP unregistered \u2014 ${i?.aiClientName??r}`,...i?.aiClientName?{aiClientName:i.aiClientName}:{}})),y.info("MCP instance unregistered",{instanceId:r,existed:a}),n.json({status:"ok",existed:a})}function FN(t,e,n){let{instanceId:r,aiClientName:i}=e.body;if(r&&i){let a=t.mcpInstances.get(r);a&&(a.aiClientName=i)}n.json({status:"ok"})}function qN(t){let n=Date.now();for(let[r,i]of t.pluginClients)n-i.lastSeen>3e4&&(t.pluginClients.delete(r),t.pendingCommands.delete(r));return AN(t),{serverInstanceId:t.instanceId,sessionId:t.sessionId,mcpVersion:We,uptime:n-t.startTime,serverStartTime:t.startTime,serverExecutable:process.execPath,serverHost:t.config.httpHost,serverPort:t.config.httpPort,serverPid:process.pid,mcpInstances:[{instanceId:t.instanceId,pid:process.pid,connectedAt:t.startTime,isServer:!0,cwd:process.cwd(),projectRoot:ml(),...t.aiClientName?{aiClientName:t.aiClientName}:{}},...Array.from(t.mcpInstances.values())],mcpInstanceCount:t.mcpInstances.size+1}}function BN(t,e){e.json(qN(t))}function ZN(t,e,n){let r=e.query.instanceId;r&&t.mcpInstances.has(r)&&(t.mcpInstances.get(r).lastSeen=Date.now()),AN(t);let i=LQ(t),a=t.sseClients.size,c={...{status:"online",connectedClients:a+i.length,queuedCommands:t.commandQueue.size,uptime:Date.now()-t.startTime,version:We,enableContextCapture:t.executionContextManager?.isEnabled()??t.config.enableContextCapture??!0,isClientMode:t.isClientMode,pid:process.pid,sessionId:t.sessionId},instanceId:t.instanceId,mcpInstanceCount:t.mcpInstances.size+1,aiClientNames:zN(t),pluginVersion:t.pluginVersion||void 0,sseClients:a,dashboardSseClients:t.dashboardSseClients?.size??0,pollingClients:i.length,pluginClients:i.map(l=>({clientId:l.clientId,projectName:l.projectName,placeName:l.placeName,pluginVersion:l.pluginVersion,lastSeen:Date.now()-l.lastSeen})),...t.isClientMode?{upstream:{reachable:t.clientModeUpstreamReachable,consecutiveFailures:t.clientModeConsecutiveHealthFailures,lastSuccessAt:t.clientModeLastHealthSuccessAt,lastFailureAt:t.clientModeLastHealthFailureAt,lastError:t.clientModeLastHealthError,baseUrl:t.baseUrl}}:{}};n.json(c)}function HN(t,e,n,r){let i=e.ip||e.socket.remoteAddress||"";if(!(i==="127.0.0.1"||i==="::1"||i==="::ffff:127.0.0.1"||i==="localhost")){y.warn("Shutdown request rejected from non-localhost",{ip:i}),n.status(403).json({error:"Forbidden: localhost only"});return}y.info("Shutdown request received, initiating graceful shutdown",{requestedBy:i,uptime:Date.now()-t.startTime}),n.json({status:"shutting_down",message:"Server will shutdown gracefully",pid:process.pid}),setTimeout(async()=>{try{await r(),y.info("Graceful shutdown completed"),process.exit(0)}catch(o){y.error("Error during graceful shutdown",o),process.exit(1)}},100)}async function jf(t){if(t.isClientMode)try{let e=await fetch(`${t.baseUrl}/connection-info`);if(e.ok)return await e.json()}catch(e){y.warn("Failed to fetch connection info from server",{error:e})}return qN(t)}pe();var zf={query_instances:{discriminator:"action",mapping:{get:"get_instance",children:"get_instance_children",find_child:"find_first_child",find_descendant:"find_first_descendant",wait_for_child:"wait_for_child",class_info:"get_class_info",search_name:"search_by_name",search_class:"search_by_class",search_property:"search_by_property",search_tag:"search_by_tag",file_tree:"get_file_tree",project_structure:"get_project_structure",descendants:"get_descendants",ancestors:"get_ancestors"},paramAliases:{search_by_name:{query:"pattern"},search_by_property:{root:"rootPath"},search_by_tag:{root:"rootPath"},get_project_structure:{root:"rootPath"}}},mutate_instances:{discriminator:"action",mapping:{create:"create_instance",create_with_props:"create_instance_with_properties",delete:"delete_instance",clone:"clone_instance",move:"move_instance",rename:"rename_instance",pivot:"pivot_to",create_tree:"create_instance_tree",mass_create:"mass_create_instances",mass_delete:"mass_delete_instances",mass_duplicate:"mass_duplicate",smart_duplicate:"smart_duplicate"},paramAliases:{clone_instance:{path:"sourcePath"}}},manage_properties:{discriminator:"action",mapping:{get:"get_property",set:"set_property",get_all:"get_all_properties",set_multiple:"set_multiple_properties",get_attr:"get_attribute",set_attr:"set_attribute",get_all_attrs:"get_all_attributes",delete_attr:"delete_attribute",add_tag:"add_tag",remove_tag:"remove_tag",check_tag:"has_tag",get_tags:"get_tags",get_tagged:"get_tagged",set_calculated:"set_calculated_property",set_relative:"set_relative_property",mass_set:"mass_set_property",mass_get:"mass_get_property",modify_children:"modify_children"},paramAliases:{get_tagged:{tagName:"tag",root:"rootPath"},set_relative_property:{amount:"value"}}},manage_scripts:{discriminator:"action",mapping:{get_source:"get_script_source",set_source:"set_script_source",create:"create_script",delete:"delete_script",edit_replace:"edit_script_lines",edit_insert:"insert_script_lines",edit_delete:"delete_script_lines",search:"search_in_scripts",replace:"replace_in_scripts",get_dependencies:"get_script_dependencies"},paramAliases:{edit_script_lines:{newLines:"newContent"},insert_script_lines:{lines:"content"},replace_in_scripts:{pattern:"searchPattern"}}},manage_lighting:{discriminator:"action",mapping:{lighting:"set_lighting",atmosphere:"set_atmosphere",sky:"set_sky",terrain_props:"set_terrain",time:"set_time_of_day"}},manage_selection:{discriminator:"action",mapping:{get:"get_selection",set:"set_selection",clear:"clear_selection",cached:"get_cached_selection",context:"get_selection_context",details:"get_selection_details",add:"add_to_selection",remove:"remove_from_selection",watch:"watch_selection"}},manage_camera:{discriminator:"action",mapping:{info:"get_camera_info",focus_path:"focus_camera_path",focus_position:"focus_camera_position",suggest:"get_suggested_camera_view"},paramAliases:{get_suggested_camera_view:{path:"targetPath"}}},manage_tween:{discriminator:"action",mapping:{create:"create_tween",play:"play_tween",pause:"pause_tween",cancel:"cancel_tween"}},manage_audio:{discriminator:"action",mapping:{play:"play_sound",stop:"stop_sound",pause:"pause_sound",resume:"resume_sound",set_listener:"set_listener"}},manage_animation:{discriminator:"action",mapping:{load:"load_animation",play:"play_animation",stop:"stop_animation",get_tracks:"get_animation_tracks"}},manage_physics:{discriminator:"action",mapping:{register_group:"register_collision_group",set_collidable:"set_collidable",get_groups:"get_collision_groups"}},manage_effects:{discriminator:"action",mapping:{emit:"emit_particles",clear:"clear_particles",toggle:"toggle_effect"}},manage_terrain:{discriminator:"action",mapping:{fill_block:"terrain_fill_block",fill_ball:"terrain_fill_ball",fill_cylinder:"terrain_fill_cylinder",fill_wedge:"terrain_fill_wedge",clear_region:"terrain_clear",clear_bounds:"terrain_clear_region",replace_material:"terrain_replace_material",colors_get:"terrain_get_material_color",colors_set:"terrain_set_material_color",read_voxel:"terrain_read_voxel",read_voxels:"terrain_read_voxels",write_voxels:"terrain_write_voxels",generate:"terrain_generate",smooth:"terrain_smooth"}},spatial_query:{discriminator:"action",mapping:{raycast:"raycast",find_ground:"find_ground",check_placement:"check_placement",multi_raycast:"multi_raycast",scan_area:"scan_area",find_flat:"find_flat_areas",find_spawn:"find_spawn_positions",analyze_walkable:"analyze_walkable_area",spatial_map:"get_spatial_map",find_space:"find_empty_space",bounds:"get_bounds",snap_grid:"snap_to_grid",collision:"check_collision"},paramAliases:{get_spatial_map:{path:"rootPath"}}},manage_assets:{discriminator:"action",mapping:{insert:"insert_model",info:"get_asset_info",search:"search_creator_store",search_insert:"search_and_insert_model",insert_free:"insert_free_model",insert_package:"insert_package",export:"export_selection"},paramAliases:{search_creator_store:{maxResults:"limit"}}},manage_sync:{discriminator:"action",mapping:{status:"sync_status",config:"sync_config",history:"sync_history",directions:"sync_directions",read_file:"sync_read_file",write_file:"sync_write_file",progress:"sync_progress"}},workspace_state:{discriminator:"action",mapping:{sync:"sync_workspace_state",snapshot:"get_workspace_snapshot",changes:"get_recent_changes",viewport:"get_viewport_info",clear_history:"clear_change_history",metadata:"get_workspace_metadata",scripts:"get_script_list",selection_info:"get_selection_info",clear_cache:"clear_state_cache"}},manage_logs:{discriminator:"action",mapping:{get:"get_output_logs",clear:"clear_output_logs",errors:"get_recent_errors"},paramAliases:{get_output_logs:{level:"type"}}},system_info:{discriminator:"action",mapping:{ping:"ping",connection:"get_connection_info",usage:"get_usage_status",place_info:"get_place_info",services:"get_services",studio_settings:"get_studio_settings",play:"start_playtest",stop:"stop_playtest",pause:"pause_playtest",resume:"resume_playtest",play_status:"get_play_status",run_test:"run_test"}}};var UQ={get_cached_selection:"internal",sync_status:"internal",sync_config:"internal",sync_history:"internal",sync_directions:"internal",sync_read_file:"internal",sync_write_file:"internal",sync_progress:"internal",get_connection_info:"internal",run_test:"internal"};function Af(t){return UQ[t]||"plugin"}var Nf=100,FQ=Object.entries(zf).reduce((t,[e,n])=>{for(let r of Object.values(n.mapping))t[r]=e;return t},{});function VN(t){return{toolName:FQ[t]||t,actionName:t}}function WN(t,e){if(typeof t.contextId=="string"||!e||typeof e!="object")return t;let n=e.contextId;return typeof n=="string"?{...t,contextId:n}:t}function GN(t,e,n){y.info("Plugin connected via SSE"),n.setHeader("Content-Type","text/event-stream"),n.setHeader("Cache-Control","no-cache"),n.setHeader("Connection","keep-alive"),t.sseClients.add(n),oS(n,{event:"command",id:kl(),data:{action:"connected",requestId:kl(),params:{serverVersion:We,timestamp:Date.now()}}});let r=setInterval(()=>{oS(n,{event:"command",id:kl(),data:{action:"keepalive",requestId:kl(),params:{timestamp:Date.now()}}})},3e4);n.on("close",()=>{y.info("Plugin disconnected from SSE"),clearInterval(r),t.sseClients.delete(n)})}function oS(t,e){let n=JSON.stringify(e.data);t.write(`event: ${e.event}
|
|
127
127
|
`),t.write(`id: ${e.id}
|
|
128
128
|
`),t.write(`data: ${n}
|
|
129
129
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
/package/docs/assets/screenshots/{connection_guide.png → plugin/connection/connection-guide.png}
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/docs/assets/screenshots/{weppy_plugin_toolbar.png → plugin/installation/toolbar-button.png}
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|