@gjlmotea/studio-sidecar-mcp 0.1.0 → 0.1.1
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/README.md +9 -5
- package/README.zh-TW.md +9 -5
- package/dist/adapters/fetch-open-cloud.d.ts.map +1 -1
- package/dist/adapters/fetch-open-cloud.js +7 -9
- package/dist/adapters/fetch-open-cloud.js.map +1 -1
- package/dist/adapters/fs-snapshot-store.d.ts +37 -0
- package/dist/adapters/fs-snapshot-store.d.ts.map +1 -0
- package/dist/adapters/fs-snapshot-store.js +328 -0
- package/dist/adapters/fs-snapshot-store.js.map +1 -0
- package/dist/adapters/mcp-studio-bridge.d.ts +2 -0
- package/dist/adapters/mcp-studio-bridge.d.ts.map +1 -1
- package/dist/adapters/mcp-studio-bridge.js +3 -3
- package/dist/adapters/mcp-studio-bridge.js.map +1 -1
- package/dist/application/roblox-service.d.ts +3 -7
- package/dist/application/roblox-service.d.ts.map +1 -1
- package/dist/application/roblox-service.js +145 -75
- package/dist/application/roblox-service.js.map +1 -1
- package/dist/composition.d.ts +0 -24
- package/dist/composition.d.ts.map +1 -1
- package/dist/composition.js +3 -141
- package/dist/composition.js.map +1 -1
- package/dist/domain/contracts.d.ts +9 -2
- package/dist/domain/contracts.d.ts.map +1 -1
- package/dist/domain/extraction.d.ts +4 -2
- package/dist/domain/extraction.d.ts.map +1 -1
- package/dist/domain/extraction.js +38 -8
- package/dist/domain/extraction.js.map +1 -1
- package/dist/domain/luau-payloads.d.ts +11 -1
- package/dist/domain/luau-payloads.d.ts.map +1 -1
- package/dist/domain/luau-payloads.js +47 -4
- package/dist/domain/luau-payloads.js.map +1 -1
- package/dist/logger.js +1 -1
- package/dist/logger.js.map +1 -1
- package/dist/ports/snapshot-store.d.ts +36 -0
- package/dist/ports/snapshot-store.d.ts.map +1 -0
- package/dist/ports/snapshot-store.js +2 -0
- package/dist/ports/snapshot-store.js.map +1 -0
- package/dist/server/create-server.d.ts +8 -1
- package/dist/server/create-server.d.ts.map +1 -1
- package/dist/server/create-server.js +9 -2
- package/dist/server/create-server.js.map +1 -1
- package/dist/server/register-tools.d.ts.map +1 -1
- package/dist/server/register-tools.js +4 -2
- package/dist/server/register-tools.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -164,7 +164,7 @@ Register with absolute paths, as above, rather than a hand-written `.mcp.json` t
|
|
|
164
164
|
| `ROBLOX_UPLOAD_ROOT` | **Folders uploads may read from**. Several are allowed, joined with the platform's path delimiter: `;` on Windows (`C:\assets\models;D:\decals`), `:` on macOS (`/Users/you/models:/Volumes/art/decals`). **When unset, every upload is refused** |
|
|
165
165
|
| `ROBLOX_STUDIO_PATH` | Studio executable; resolved automatically from the current installation when unset |
|
|
166
166
|
|
|
167
|
-
`ROBLOX_UPLOAD_ROOT` is a deliberate guard rail: uploading sends a local file to an external service, and for Models it cannot be undone, so the MCP client may not pick arbitrary file paths.
|
|
167
|
+
`ROBLOX_UPLOAD_ROOT` is a deliberate guard rail: uploading sends a local file to an external service, and for Models it cannot be undone, so the MCP client may not pick arbitrary file paths. Both the file and the allowed folders are resolved with `realpath` before they are compared, so a symlink inside an allowed folder cannot point the upload elsewhere. The file is opened once, checked again through that open handle (still inside an allowed folder, still the same file), and the bytes that go out are read from that handle, so swapping the path after the check changes nothing. A file with more than one hard link is refused, because a hard link inside an allowed folder can be a file from anywhere else on the same volume; upload a copy instead.
|
|
168
168
|
|
|
169
169
|
## What the tools know that the built-in server does not tell you
|
|
170
170
|
|
|
@@ -184,9 +184,13 @@ Each of these was found on real hardware and has a regression test.
|
|
|
184
184
|
2. The UTF-8-safe cut point is decided in Lua and reported back, and the caller continues from there, so the two sides never compute boundaries separately.
|
|
185
185
|
3. A byte-count check per file (`Buffer.byteLength`, not `String.length`). Any mismatch aborts the whole extraction, because a partially correct snapshot looks like success.
|
|
186
186
|
|
|
187
|
-
Each call stages its data under its own key, `shared.RBXEXTRACT_<32 hex digits>` (Lua VM memory only, never the DataModel or the `.rbxl`), and clears it afterwards, so overlapping calls never read each other's data.
|
|
187
|
+
Each call stages its data under its own key, `shared.RBXEXTRACT_<32 hex digits>` (Lua VM memory only, never the DataModel or the `.rbxl`), and clears it afterwards, so overlapping calls never read each other's data. If clearing fails, the next call that reaches the same Studio clears it again, up to three attempts in all. Every key also records when it was last used, and each new key first clears any key that has sat idle for 15 minutes, so a key left behind by a failed release, a shutdown or a crash goes away on the next call to that Studio. During shutdown the server never reconnects to Studio just to clean up.
|
|
188
188
|
|
|
189
|
-
|
|
189
|
+
Instance names can be almost anything, including `..`, path separators and Windows device names such as `CON` or `NUL`, so every path segment is sanitized. A device name gets a trailing `_` (`CON` becomes `CON_.server.luau`) on every platform, so a snapshot made on macOS still opens on Windows, and the final path is checked to be inside the output folder before writing.
|
|
190
|
+
|
|
191
|
+
The whole new snapshot is first written to a fresh folder that the extraction creates inside the output folder (`.studio-sidecar-staging-<random>`). Only after every file is there does the switch start: the files of the previous snapshot are moved aside, and the new ones are put in place with hard links that fail if the name is taken (or, on file systems without hard links, with exclusive creation). If anything fails during the switch, the new files are removed and the previous snapshot is moved back, so a failed run leaves the previous snapshot as it was. Extractions into the same folder run one after another. A run that is killed midway can leave a `.studio-sidecar-staging-*` folder behind; it holds only snapshot files and is safe to delete.
|
|
192
|
+
|
|
193
|
+
Nothing is ever overwritten. When a name is already taken, including by a name the file system treats as equal (`Σ`, `ς` and `σ` on a case-insensitive volume), the script gets the next `~n` suffix, and `scene/scripts.json` records the path actually written. Re-running into the same folder replaces only the files listed in that folder's previous `scene/scripts.json`; a folder whose `scene/` files were not written by Studio Sidecar is refused. If one of those files has been turned into a hard link, only that name is removed, and the content under the other name stays as it was.
|
|
190
194
|
|
|
191
195
|
**The Studio bridge must live as long as the server.** Closing it after every call makes the first call succeed and every later one fail with "Unable to reach Roblox Studio right now", which sounds like Studio is closed. It is not: closing kills the stub, and the next call races the broker. The same message also appears during a cold start before a fresh stub reaches the broker, so the bridge retries the Studio listing on that one message, and only that one, for up to 20 seconds; the wait grows by 250 ms per attempt, up to 1.5 seconds. Once the server starts shutting down, the bridge refuses new connections, so a call that is still finishing cannot start a fresh stub.
|
|
192
196
|
|
|
@@ -210,8 +214,8 @@ Platform differences sit behind `ports/process-inspector.ts`; broker detection,
|
|
|
210
214
|
- **`roblox_luau_safe` is not a new way to run arbitrary code.** `execute_luau` is already in the client's hands; this tool adds a mandatory PlaceId guard, one `pcall` around the whole chunk and chunked results in front of the same entry point. The `pcall` catches a runtime error and returns it as a readable value; the code after the error does not run, and changes made before the error are not rolled back. It is not a sandbox.
|
|
211
215
|
- **No reverse engineering of the broker's private protocol on 13469.** Talking to `StudioMCP` as an ordinary MCP client makes that unnecessary.
|
|
212
216
|
- **No "remove from inventory".** That goes through `inventory.roblox.com` with a signed-in session cookie and CSRF token, which an Open Cloud API key cannot do.
|
|
213
|
-
- **Known limitation: a folder
|
|
214
|
-
- **Known limitation:
|
|
217
|
+
- **Known limitation: a folder that already exists in the output folder can be swapped during the switch.** Files are written into the fresh staging folder, but putting them in place still walks the output folder's existing sub-folders by path. Each level is resolved with `realpath` and checked to be inside the output folder first; another program on the same machine could still replace a folder with a symlink between that check and the move (time-of-check/time-of-use). The move only ever creates a new name, so at worst one new file appears outside the output folder; nothing outside is overwritten or deleted.
|
|
218
|
+
- **Known limitation: an unreleased buffer lasts until the next call.** A Luau buffer that could not be released stays in Studio's `shared` table until a later call reaches the same Studio (and, for the idle sweep, until it has been idle for 15 minutes), or until Studio closes.
|
|
215
219
|
|
|
216
220
|
## Development
|
|
217
221
|
|
package/README.zh-TW.md
CHANGED
|
@@ -164,7 +164,7 @@ claude mcp add studio-sidecar --scope user -- "$(which node)" "$PWD/dist/index.j
|
|
|
164
164
|
| `ROBLOX_UPLOAD_ROOT` | **上傳可以讀取的資料夾**。可以有多個,以平台的路徑分隔符連接:Windows 用 `;`(`C:\assets\models;D:\decals`),macOS 用 `:`(`/Users/you/models:/Volumes/art/decals`)。**未設定時所有上傳一律拒絕** |
|
|
165
165
|
| `ROBLOX_STUDIO_PATH` | Studio 執行檔;未設定時自動從目前的安裝解析 |
|
|
166
166
|
|
|
167
|
-
`ROBLOX_UPLOAD_ROOT` 是刻意設下的護欄:上傳會把本機檔案送到外部服務,對 Model 來說還無法撤回,所以不讓 MCP client
|
|
167
|
+
`ROBLOX_UPLOAD_ROOT` 是刻意設下的護欄:上傳會把本機檔案送到外部服務,對 Model 來說還無法撤回,所以不讓 MCP client 任意指定檔案路徑。檔案與允許的資料夾都先用 `realpath` 解析再比對,所以允許資料夾裡的 symlink 不能把上傳導到別處。檔案只打開一次,透過這個打開的 handle 再確認一次(仍在允許的資料夾內、仍是同一個檔案),送出去的位元組也從這個 handle 讀,所以檢查之後才把路徑換掉不會有任何作用。硬連結超過一個的檔案一律拒絕:允許資料夾裡的硬連結可以就是同一個磁碟上任何地方的檔案;請改傳一份複本。
|
|
168
168
|
|
|
169
169
|
## 內建 server 沒告訴你的事
|
|
170
170
|
|
|
@@ -184,9 +184,13 @@ claude mcp add studio-sidecar --scope user -- "$(which node)" "$PWD/dist/index.j
|
|
|
184
184
|
2. UTF-8 安全的切點由 Lua 決定並回報,呼叫端從那裡接著讀,兩邊從不各自計算邊界。
|
|
185
185
|
3. 逐檔校驗位元組數(用 `Buffer.byteLength`,不是 `String.length`)。任何一檔對不上就中止整次抽取,因為局部正確的快照看起來像成功。
|
|
186
186
|
|
|
187
|
-
每次呼叫把資料暫存在自己的 key 底下:`shared.RBXEXTRACT_<32 位十六進位>`(只在 Lua VM 記憶體裡,不進 DataModel 也不進 `.rbxl
|
|
187
|
+
每次呼叫把資料暫存在自己的 key 底下:`shared.RBXEXTRACT_<32 位十六進位>`(只在 Lua VM 記憶體裡,不進 DataModel 也不進 `.rbxl`),用完就清掉,所以重疊的呼叫不會讀到彼此的資料。清不掉時,下一次碰到同一個 Studio 的呼叫會再清一次,總共最多試三次。每把 key 也記著最後一次使用的時間,每建立一把新的 key 之前,會先清掉閒置超過 15 分鐘的 key,所以釋放失敗、關機或當掉時留下的 key,會在下一次呼叫那個 Studio 時消失。關機途中 server 絕不為了清理而重新連上 Studio。
|
|
188
188
|
|
|
189
|
-
|
|
189
|
+
實例名稱幾乎可以是任何字串,包括 `..`、路徑分隔符,以及 `CON`、`NUL` 這類 Windows 裝置名,所以每段路徑都會先清理。裝置名在每個平台上都會補一個 `_`(`CON` 變成 `CON_.server.luau`),在 macOS 抽出的快照搬到 Windows 也打得開;寫入前也會確認最終路徑落在輸出資料夾內。
|
|
190
|
+
|
|
191
|
+
整份新快照先寫進抽取在輸出資料夾裡新建的資料夾(`.studio-sidecar-staging-<隨機字串>`)。所有檔案都到齊之後才開始切換:上一次快照的檔案先移到一旁,新檔以硬連結放到正式位置,名字已被占用時硬連結會失敗(不支援硬連結的檔案系統改用獨佔建立)。切換途中任何一步失敗,就收掉新檔、把上一次的快照搬回原位,所以失敗的抽取會讓上一次的快照維持原樣。寫到同一個資料夾的抽取依序進行。中途被強制結束的抽取可能留下 `.studio-sidecar-staging-*` 資料夾,裡面只有快照檔,可以直接刪掉。
|
|
192
|
+
|
|
193
|
+
從不覆蓋任何東西。名字已經被占用時(包括檔案系統視為同名的名字,例如不分大小寫的磁碟上的 `Σ`、`ς` 與 `σ`),該腳本改用下一個 `~n` 後綴,`scene/scripts.json` 記下實際寫入的路徑。重跑到同一個資料夾時,只換掉該資料夾上一次 `scene/scripts.json` 列出的檔案;`scene/` 底下的檔案不是 Studio Sidecar 寫的資料夾會被拒絕。那些檔案之中若有被做成硬連結的,拿掉的只是這個名字,另一個名字底下的內容維持原樣。
|
|
190
194
|
|
|
191
195
|
**Studio 橋接必須和 server 活得一樣久。** 每次呼叫後就關閉它,會讓第一次呼叫成功、之後每次都失敗,錯誤訊息是 "Unable to reach Roblox Studio right now",聽起來像 Studio 沒開。其實不是:關閉會殺掉 stub,下一次呼叫就和 broker 搶時間。冷啟動時,新的 stub 還沒連上 broker 之前也會出現同一句訊息,所以橋接只針對這一句訊息重試列出 Studio 的呼叫,最多 20 秒;每次重試前的等待比上一次多 250 毫秒,最長 1.5 秒。server 開始關機後,橋接拒絕任何新連線,所以那時還在收尾的呼叫不會再生出一隻新的 stub。
|
|
192
196
|
|
|
@@ -210,8 +214,8 @@ claude mcp add studio-sidecar --scope user -- "$(which node)" "$PWD/dist/index.j
|
|
|
210
214
|
- **`roblox_luau_safe` 不是執行任意程式碼的新途徑。** `execute_luau` 本來就在 client 手上;這個工具只是在同一個入口前加上強制的 PlaceId 守衛、包住整段程式的單一 `pcall` 與分塊結果。`pcall` 會接住執行期錯誤,把它變成可讀的回傳值;錯誤之後的程式不會執行,錯誤之前做的修改也不會復原。它不是沙箱。
|
|
211
215
|
- **不逆向 broker 在 13469 上的私有協定。** 以一般 MCP client 的身分和 `StudioMCP` 溝通,就沒有這個必要。
|
|
212
216
|
- **不提供「從物品欄移除」。** 那要透過 `inventory.roblox.com`,並帶登入後的 session cookie 與 CSRF token,Open Cloud API 金鑰做不到。
|
|
213
|
-
-
|
|
214
|
-
-
|
|
217
|
+
- **已知限制:輸出資料夾裡既有的資料夾可能在切換途中被換掉。** 檔案寫在新建的暫存資料夾裡,但放到正式位置時,仍是照路徑走輸出資料夾裡既有的子資料夾。每一層都先用 `realpath` 解析並確認落在輸出資料夾內;同一台機器上的其他程式仍可以在檢查與搬移之間把某個資料夾換成 symlink(time-of-check/time-of-use)。搬移只會建立新的名字,最壞的情況是輸出資料夾外多出一個新檔,外面的東西不會被覆蓋或刪除。
|
|
218
|
+
- **已知限制:沒釋放的 buffer 會留到下一次呼叫。** 釋放不掉的 Luau buffer 會留在 Studio 的 `shared` 表裡,直到之後有呼叫碰到同一個 Studio(閒置清理還要等它閒置滿 15 分鐘),或 Studio 關閉為止。
|
|
215
219
|
|
|
216
220
|
## 開發
|
|
217
221
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fetch-open-cloud.d.ts","sourceRoot":"","sources":["../../src/adapters/fetch-open-cloud.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"fetch-open-cloud.d.ts","sourceRoot":"","sources":["../../src/adapters/fetch-open-cloud.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvG,OAAO,EAAkB,KAAK,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAqCxE,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,yCAAyC;IACzC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IAClC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAChD,CAAC;AAEF,qBAAa,cAAe,YAAW,SAAS;;IAC9C,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAQ7B,YAAY,OAAO,GAAE,qBAA0B,EAO9C;IA0EK,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAG/C;IAEK,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,WAAW,CAAC,CAUzD;IAEK,aAAa,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,WAAW,CAAC,CAUhE;IAEK,cAAc,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,WAAW,CAAC,CAqBjE;IAEK,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAOnD;IAEK,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAOnD;CACF"}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import { basename } from 'node:path';
|
|
3
1
|
import { resolveUpload } from '../domain/asset-types.js';
|
|
4
2
|
import { OpenCloudError } from '../ports/open-cloud.js';
|
|
5
3
|
const BASE = 'https://apis.roblox.com/assets/v1';
|
|
@@ -89,11 +87,11 @@ export class FetchOpenCloud {
|
|
|
89
87
|
}
|
|
90
88
|
return toRecord(operation.response);
|
|
91
89
|
}
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
/** 內容由呼叫端給:這一層不碰檔案系統,送出去的就是服務層驗過、讀好的那份位元組。 */
|
|
91
|
+
#multipart(fileName, content, contentType, request) {
|
|
94
92
|
const form = new FormData();
|
|
95
93
|
form.set('request', JSON.stringify(request));
|
|
96
|
-
form.set('fileContent', new Blob([new Uint8Array(
|
|
94
|
+
form.set('fileContent', new Blob([new Uint8Array(content)], { type: contentType }), fileName);
|
|
97
95
|
return form;
|
|
98
96
|
}
|
|
99
97
|
async get(assetId) {
|
|
@@ -101,8 +99,8 @@ export class FetchOpenCloud {
|
|
|
101
99
|
return toRecord(raw);
|
|
102
100
|
}
|
|
103
101
|
async create(request) {
|
|
104
|
-
const { contentType } = resolveUpload(request.assetType, request.
|
|
105
|
-
const form =
|
|
102
|
+
const { contentType } = resolveUpload(request.assetType, request.fileName);
|
|
103
|
+
const form = this.#multipart(request.fileName, request.content, contentType, {
|
|
106
104
|
assetType: request.assetType,
|
|
107
105
|
displayName: request.displayName,
|
|
108
106
|
description: request.description,
|
|
@@ -112,8 +110,8 @@ export class FetchOpenCloud {
|
|
|
112
110
|
return await this.#await(operation);
|
|
113
111
|
}
|
|
114
112
|
async updateContent(request) {
|
|
115
|
-
const { contentType } = resolveUpload(request.assetType, request.
|
|
116
|
-
const form =
|
|
113
|
+
const { contentType } = resolveUpload(request.assetType, request.fileName);
|
|
114
|
+
const form = this.#multipart(request.fileName, request.content, contentType, { assetId: request.assetId });
|
|
117
115
|
// ⚠ 換內容**不要**帶 updateMask:`updateMask=fileContent` 會回
|
|
118
116
|
// `INVALID_ARGUMENT — Unknown field file_content`。
|
|
119
117
|
const operation = (await this.#send(`/assets/${encodeURIComponent(request.assetId)}`, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fetch-open-cloud.js","sourceRoot":"","sources":["../../src/adapters/fetch-open-cloud.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"fetch-open-cloud.js","sourceRoot":"","sources":["../../src/adapters/fetch-open-cloud.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { CommittedSnapshot, SnapshotPlan, SnapshotStore } from '../ports/snapshot-store.js';
|
|
2
|
+
/** 暫存目錄的名字前綴。建在輸出目錄裡,切換時的搬移才保證在同一個檔案系統上。 */
|
|
3
|
+
export declare const STAGING_PREFIX = ".studio-sidecar-staging-";
|
|
4
|
+
export type FsSnapshotStoreOptions = {
|
|
5
|
+
/**
|
|
6
|
+
* 建立硬連結;測試用來模擬不支援硬連結的檔案系統(FAT、exFAT)。
|
|
7
|
+
* 目標已經存在時必須以 `code: 'EEXIST'` 失敗。
|
|
8
|
+
*/
|
|
9
|
+
readonly link?: (existing: string, created: string) => Promise<void>;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* 把抽取結果寫成輸出目錄裡的快照。
|
|
13
|
+
*
|
|
14
|
+
* 🔴 0.1.0 先刪上一次的快照再寫新的,寫到一半失敗(磁碟滿、路徑被檔案擋住)就兩份都沒了。
|
|
15
|
+
* 現在分兩段:
|
|
16
|
+
* 1. **寫**:整份新快照先寫進輸出目錄裡一個全新的暫存目錄(`mkdtemp`,名字隨機、只有本行程知道),
|
|
17
|
+
* 每個檔都獨佔建立。這一段失敗只會丟掉暫存目錄,上一次的快照完全沒動。
|
|
18
|
+
* 2. **切換**:上一次快照的檔搬進暫存目錄,新檔以硬連結放到正式位置(目標已存在就失敗,
|
|
19
|
+
* 不會覆蓋任何東西)。中途失敗就把放上去的新檔收掉、舊檔搬回原位。
|
|
20
|
+
*
|
|
21
|
+
* 同一個輸出目錄(以 realpath 認)的寫入排隊依序做:兩次抽取交錯時,後一次會把前一次當成別人的檔,
|
|
22
|
+
* 改用 `~2` 的名字,而且前一次寫的檔不在最後那份 scripts.json 裡,下次就再也清不掉。
|
|
23
|
+
*
|
|
24
|
+
* 硬連結:本工具寫的檔都只有一個名字。上一次快照列出的檔就算被做成硬連結,搬走與刪除的也只是
|
|
25
|
+
* 這個名字,別的名字底下的內容不受影響;寫入一律是建立新檔,從不打開已經存在的檔,
|
|
26
|
+
* 所以也不可能透過硬連結寫進別的檔。
|
|
27
|
+
*
|
|
28
|
+
* 輸出目錄底下既有的資料夾仍是用路徑走的:每一層都先 realpath 確認還在輸出目錄裡才往下,
|
|
29
|
+
* 但檢查與搬移之間被同機器上的其他程式換成 symlink 的話擋不住(README 的已知限制)。
|
|
30
|
+
* 那種情況下最多是在外面多建一個新檔,不會覆蓋或刪除外面的東西。
|
|
31
|
+
*/
|
|
32
|
+
export declare class FsSnapshotStore implements SnapshotStore {
|
|
33
|
+
#private;
|
|
34
|
+
constructor(options?: FsSnapshotStoreOptions);
|
|
35
|
+
commit(outDir: string, plan: SnapshotPlan): Promise<CommittedSnapshot>;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=fs-snapshot-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fs-snapshot-store.d.ts","sourceRoot":"","sources":["../../src/adapters/fs-snapshot-store.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,iBAAiB,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAEjG,4CAA4C;AAC5C,eAAO,MAAM,cAAc,6BAA6B,CAAC;AAkCzD,MAAM,MAAM,sBAAsB,GAAG;IACnC;;;OAGG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACtE,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,eAAgB,YAAW,aAAa;;IAInD,YAAY,OAAO,GAAE,sBAA2B,EAE/C;IAEK,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAK3E;CAiLF"}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { link as fsLink, lstat, mkdir, mkdtemp, open, realpath, rename, rm, rmdir, unlink } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { SCENE_FILES, sanitizeSegment, scriptFilesInManifest } from '../domain/extraction.js';
|
|
5
|
+
import { isPathInside } from '../domain/path-containment.js';
|
|
6
|
+
/** 暫存目錄的名字前綴。建在輸出目錄裡,切換時的搬移才保證在同一個檔案系統上。 */
|
|
7
|
+
export const STAGING_PREFIX = '.studio-sidecar-staging-';
|
|
8
|
+
/** 一份腳本在輸出目錄裡最多試幾個名字。 */
|
|
9
|
+
const MAX_NAME_ATTEMPTS = 1_000;
|
|
10
|
+
/**
|
|
11
|
+
* 獨佔建立:已經存在的檔案、資料夾或 symlink 都不會被打開,連檢查與開檔之間才冒出來的也一樣。
|
|
12
|
+
* O_NOFOLLOW 是額外的一層(Windows 沒有這個旗標,退回 0)。
|
|
13
|
+
*/
|
|
14
|
+
const EXCLUSIVE_CREATE = constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0);
|
|
15
|
+
const [PLACE_FILE, MANIFEST_FILE] = SCENE_FILES;
|
|
16
|
+
const code = (error) => error?.code;
|
|
17
|
+
const ignoreMissing = (error) => {
|
|
18
|
+
if (code(error) === 'ENOENT')
|
|
19
|
+
return undefined;
|
|
20
|
+
throw error;
|
|
21
|
+
};
|
|
22
|
+
const messageOf = (error) => (error instanceof Error ? error.message : String(error));
|
|
23
|
+
/**
|
|
24
|
+
* 把抽取結果寫成輸出目錄裡的快照。
|
|
25
|
+
*
|
|
26
|
+
* 🔴 0.1.0 先刪上一次的快照再寫新的,寫到一半失敗(磁碟滿、路徑被檔案擋住)就兩份都沒了。
|
|
27
|
+
* 現在分兩段:
|
|
28
|
+
* 1. **寫**:整份新快照先寫進輸出目錄裡一個全新的暫存目錄(`mkdtemp`,名字隨機、只有本行程知道),
|
|
29
|
+
* 每個檔都獨佔建立。這一段失敗只會丟掉暫存目錄,上一次的快照完全沒動。
|
|
30
|
+
* 2. **切換**:上一次快照的檔搬進暫存目錄,新檔以硬連結放到正式位置(目標已存在就失敗,
|
|
31
|
+
* 不會覆蓋任何東西)。中途失敗就把放上去的新檔收掉、舊檔搬回原位。
|
|
32
|
+
*
|
|
33
|
+
* 同一個輸出目錄(以 realpath 認)的寫入排隊依序做:兩次抽取交錯時,後一次會把前一次當成別人的檔,
|
|
34
|
+
* 改用 `~2` 的名字,而且前一次寫的檔不在最後那份 scripts.json 裡,下次就再也清不掉。
|
|
35
|
+
*
|
|
36
|
+
* 硬連結:本工具寫的檔都只有一個名字。上一次快照列出的檔就算被做成硬連結,搬走與刪除的也只是
|
|
37
|
+
* 這個名字,別的名字底下的內容不受影響;寫入一律是建立新檔,從不打開已經存在的檔,
|
|
38
|
+
* 所以也不可能透過硬連結寫進別的檔。
|
|
39
|
+
*
|
|
40
|
+
* 輸出目錄底下既有的資料夾仍是用路徑走的:每一層都先 realpath 確認還在輸出目錄裡才往下,
|
|
41
|
+
* 但檢查與搬移之間被同機器上的其他程式換成 symlink 的話擋不住(README 的已知限制)。
|
|
42
|
+
* 那種情況下最多是在外面多建一個新檔,不會覆蓋或刪除外面的東西。
|
|
43
|
+
*/
|
|
44
|
+
export class FsSnapshotStore {
|
|
45
|
+
#queues = new Map();
|
|
46
|
+
#link;
|
|
47
|
+
constructor(options = {}) {
|
|
48
|
+
this.#link = options.link ?? fsLink;
|
|
49
|
+
}
|
|
50
|
+
async commit(outDir, plan) {
|
|
51
|
+
const root = resolve(outDir);
|
|
52
|
+
await mkdir(root, { recursive: true });
|
|
53
|
+
const realRoot = await realpath(root);
|
|
54
|
+
return await this.#serialized(realRoot, async () => await this.#commit(root, realRoot, plan));
|
|
55
|
+
}
|
|
56
|
+
async #serialized(key, work) {
|
|
57
|
+
const previous = this.#queues.get(key) ?? Promise.resolve();
|
|
58
|
+
const run = previous.then(work);
|
|
59
|
+
const tail = run.then(() => undefined, () => undefined);
|
|
60
|
+
this.#queues.set(key, tail);
|
|
61
|
+
try {
|
|
62
|
+
return await run;
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
if (this.#queues.get(key) === tail)
|
|
66
|
+
this.#queues.delete(key);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async #commit(root, realRoot, plan) {
|
|
70
|
+
// 認不得上一次的快照就整個中止:還沒寫任何東西,也沒動任何東西。
|
|
71
|
+
const previous = await previousSnapshotFiles(root, realRoot);
|
|
72
|
+
const staging = await mkdtemp(join(root, STAGING_PREFIX));
|
|
73
|
+
let keepStaging = false;
|
|
74
|
+
try {
|
|
75
|
+
// ── 1. 寫:新快照整份進暫存目錄。名字用流水號,真正的檔名切換時才決定。 ──
|
|
76
|
+
const fresh = join(staging, 'new');
|
|
77
|
+
const aside = join(staging, 'old');
|
|
78
|
+
await mkdir(fresh);
|
|
79
|
+
await mkdir(aside);
|
|
80
|
+
const stagedScripts = [];
|
|
81
|
+
for (const [i, script] of plan.scripts.entries()) {
|
|
82
|
+
const staged = join(fresh, `script-${String(i)}`);
|
|
83
|
+
await writeExclusive(staged, script.content);
|
|
84
|
+
stagedScripts.push(staged);
|
|
85
|
+
}
|
|
86
|
+
const stagedPlace = join(fresh, 'place.json');
|
|
87
|
+
await writeExclusive(stagedPlace, plan.place);
|
|
88
|
+
// ── 2. 切換 ──────────────────────────────────────────────────────
|
|
89
|
+
const journal = { moved: [], placed: [], createdDirs: [] };
|
|
90
|
+
try {
|
|
91
|
+
// 先全部看過再搬:中途發現穿越就一個都不動。scripts.json 排最後。
|
|
92
|
+
const doomed = [];
|
|
93
|
+
for (const relative of new Set([...previous, PLACE_FILE, MANIFEST_FILE])) {
|
|
94
|
+
const { target, stat } = await locate(root, realRoot, relative);
|
|
95
|
+
// 只搬一般檔。symlink 或資料夾不是本工具寫的,留著(新檔撞到它會改名或中止)。
|
|
96
|
+
if (stat?.isFile() === true)
|
|
97
|
+
doomed.push(target);
|
|
98
|
+
}
|
|
99
|
+
for (const [i, target] of doomed.entries()) {
|
|
100
|
+
const to = join(aside, String(i));
|
|
101
|
+
try {
|
|
102
|
+
await rename(target, to);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
// 不分大小寫的檔案系統上,清單裡只差大小寫的兩筆是同一個檔,第二筆已經搬走了。
|
|
106
|
+
if (code(error) === 'ENOENT')
|
|
107
|
+
continue;
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
journal.moved.push({ from: target, to });
|
|
111
|
+
}
|
|
112
|
+
const files = [];
|
|
113
|
+
for (const [i, script] of plan.scripts.entries()) {
|
|
114
|
+
const staged = stagedScripts[i];
|
|
115
|
+
if (staged === undefined)
|
|
116
|
+
throw new Error(`No staged file for script #${String(i)}.`);
|
|
117
|
+
files.push(await this.#placeFirstFree(root, realRoot, staged, script.target, script.content, journal));
|
|
118
|
+
}
|
|
119
|
+
await this.#placeScene(root, realRoot, stagedPlace, PLACE_FILE, plan.place, journal);
|
|
120
|
+
const manifest = plan.manifest(files);
|
|
121
|
+
const stagedManifest = join(fresh, 'scripts.json');
|
|
122
|
+
await writeExclusive(stagedManifest, manifest);
|
|
123
|
+
await this.#placeScene(root, realRoot, stagedManifest, MANIFEST_FILE, manifest, journal);
|
|
124
|
+
// 上一次快照的資料夾若因此變空就收掉。rmdir 只收空資料夾,裡面還有別人的東西就留著。
|
|
125
|
+
await removeEmptyDirs(root, journal.moved.map((entry) => dirname(entry.from)));
|
|
126
|
+
return {
|
|
127
|
+
files,
|
|
128
|
+
written: [...files, PLACE_FILE, MANIFEST_FILE].map((relative) => join(root, ...relative.split('/'))),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
const stranded = await rollback(journal);
|
|
133
|
+
await removeEmptyDirs(root, journal.createdDirs);
|
|
134
|
+
if (stranded.length === 0)
|
|
135
|
+
throw error;
|
|
136
|
+
keepStaging = true;
|
|
137
|
+
throw new Error(`${messageOf(error)}\nRestoring the previous snapshot failed for ${String(stranded.length)} file(s); `
|
|
138
|
+
+ `they are kept in ${aside} (original paths: ${stranded.join(', ')}).`, { cause: error });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
if (!keepStaging)
|
|
143
|
+
await rm(staging, { recursive: true, force: true });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/** 依序試名稱表的名字與它的後備名字,放進第一個沒被占用的。回傳實際的相對路徑。 */
|
|
147
|
+
async #placeFirstFree(root, realRoot, staged, planned, content, journal) {
|
|
148
|
+
for (let attempt = 0; attempt < MAX_NAME_ATTEMPTS; attempt += 1) {
|
|
149
|
+
const relative = (attempt === 0 ? planned.path : planned.fallback(attempt))
|
|
150
|
+
.split('/')
|
|
151
|
+
.map(sanitizeSegment)
|
|
152
|
+
.join('/');
|
|
153
|
+
try {
|
|
154
|
+
await this.#place(root, realRoot, staged, relative, content, journal);
|
|
155
|
+
return relative;
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
if (code(error) !== 'EEXIST')
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
throw new Error(`${MAX_NAME_ATTEMPTS} candidate file names are all taken in the output folder (starting at ${planned.path}).`);
|
|
163
|
+
}
|
|
164
|
+
/** scene 檔的名字固定,被別人占走(不是本工具寫的一般檔,所以沒被搬走)就中止。 */
|
|
165
|
+
async #placeScene(root, realRoot, staged, relative, content, journal) {
|
|
166
|
+
try {
|
|
167
|
+
await this.#place(root, realRoot, staged, relative, content, journal);
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
if (code(error) !== 'EEXIST')
|
|
171
|
+
throw error;
|
|
172
|
+
throw new Error(`${relative} in the output folder is not a regular file written by this tool; refusing to replace it: `
|
|
173
|
+
+ `${join(root, ...relative.split('/'))}`, { cause: error });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* 把暫存檔放到輸出目錄裡的 `relative`,目標已經存在時以 EEXIST 失敗。
|
|
178
|
+
*
|
|
179
|
+
* 先用硬連結:一步完成、檔案一出現就是完整內容。不支援硬連結的檔案系統改成獨佔建立再寫一次,
|
|
180
|
+
* 寫到一半失敗就把這個自己剛建的檔刪掉。
|
|
181
|
+
*/
|
|
182
|
+
async #place(root, realRoot, staged, relative, content, journal) {
|
|
183
|
+
const segments = relative.split('/');
|
|
184
|
+
const fileName = segments.pop() ?? '_';
|
|
185
|
+
const dir = await ensureDirInside(root, realRoot, segments, journal.createdDirs);
|
|
186
|
+
const target = join(dir, fileName);
|
|
187
|
+
if (!isPathInside(root, target))
|
|
188
|
+
throw new Error(`Output path escapes the output folder: ${relative}`);
|
|
189
|
+
try {
|
|
190
|
+
await this.#link(staged, target);
|
|
191
|
+
journal.placed.push(target);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
if (code(error) === 'EEXIST')
|
|
196
|
+
throw error;
|
|
197
|
+
// 其他錯誤(EPERM、ENOTSUP…)多半是檔案系統不支援硬連結;換獨佔建立。
|
|
198
|
+
// 真正的問題(資料夾不見、沒有權限)會在下面的 open 以同樣的錯誤再出現一次。
|
|
199
|
+
}
|
|
200
|
+
const handle = await open(target, EXCLUSIVE_CREATE, 0o666);
|
|
201
|
+
// 從這裡起這個檔是自己建的,失敗時可以刪。
|
|
202
|
+
journal.placed.push(target);
|
|
203
|
+
try {
|
|
204
|
+
await handle.writeFile(content, 'utf8');
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
await handle.close();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/** 獨佔建立並寫入。 */
|
|
212
|
+
async function writeExclusive(path, content) {
|
|
213
|
+
const handle = await open(path, EXCLUSIVE_CREATE, 0o666);
|
|
214
|
+
try {
|
|
215
|
+
await handle.writeFile(content, 'utf8');
|
|
216
|
+
}
|
|
217
|
+
finally {
|
|
218
|
+
await handle.close();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/** 回傳輸出目錄內這個相對路徑的狀態;所在資料夾透過 symlink 跑到外面時直接中止。 */
|
|
222
|
+
async function locate(root, realRoot, relative) {
|
|
223
|
+
const target = join(root, ...relative.split('/'));
|
|
224
|
+
const realParent = await realpath(dirname(target)).catch(ignoreMissing);
|
|
225
|
+
if (realParent === undefined)
|
|
226
|
+
return { target, stat: undefined };
|
|
227
|
+
if (!isPathInside(realRoot, realParent)) {
|
|
228
|
+
throw new Error(`Output path escapes the output folder through a symlink: ${relative}`);
|
|
229
|
+
}
|
|
230
|
+
return { target, stat: await lstat(target).catch(ignoreMissing) };
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* 逐層建出資料夾。每一層都先 realpath 確認還在輸出目錄裡才往下建,
|
|
234
|
+
* 不然 mkdir 會順著 symlink 在外面建出資料夾。
|
|
235
|
+
*/
|
|
236
|
+
async function ensureDirInside(root, realRoot, segments, createdDirs) {
|
|
237
|
+
let dir = root;
|
|
238
|
+
for (const segment of segments) {
|
|
239
|
+
dir = join(dir, segment);
|
|
240
|
+
try {
|
|
241
|
+
await mkdir(dir);
|
|
242
|
+
createdDirs.push(dir);
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
if (code(error) !== 'EEXIST')
|
|
246
|
+
throw error;
|
|
247
|
+
}
|
|
248
|
+
if (!isPathInside(realRoot, await realpath(dir))) {
|
|
249
|
+
throw new Error(`Output path escapes the output folder through a symlink: ${dir}`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return dir;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* 讀上一次抽取留下的 `scene/scripts.json`,回傳它寫過的腳本檔(相對路徑)。
|
|
256
|
+
*
|
|
257
|
+
* 🔴 scene 檔不是本工具寫的(沒有認得出來的 scripts.json)時整個中止:
|
|
258
|
+
* 那兩個檔名是固定的,不能換名字,也不能蓋掉別人的檔。
|
|
259
|
+
*/
|
|
260
|
+
async function previousSnapshotFiles(root, realRoot) {
|
|
261
|
+
const manifest = await locate(root, realRoot, MANIFEST_FILE);
|
|
262
|
+
if (manifest.stat === undefined) {
|
|
263
|
+
if ((await locate(root, realRoot, PLACE_FILE)).stat !== undefined) {
|
|
264
|
+
throw new Error(`The output folder already has ${PLACE_FILE} but no ${MANIFEST_FILE}, so it was not written by this tool. `
|
|
265
|
+
+ `Nothing was changed. Use an empty folder or remove that file: ${join(root, PLACE_FILE)}`);
|
|
266
|
+
}
|
|
267
|
+
return [];
|
|
268
|
+
}
|
|
269
|
+
if (!manifest.stat.isFile()) {
|
|
270
|
+
throw new Error(`${MANIFEST_FILE} in the output folder is not a regular file; refusing to use it: ${manifest.target}`);
|
|
271
|
+
}
|
|
272
|
+
const handle = await open(manifest.target, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
273
|
+
let text;
|
|
274
|
+
try {
|
|
275
|
+
text = await handle.readFile('utf8');
|
|
276
|
+
}
|
|
277
|
+
finally {
|
|
278
|
+
await handle.close();
|
|
279
|
+
}
|
|
280
|
+
const scripts = scriptFilesInManifest(text);
|
|
281
|
+
if (scripts === undefined) {
|
|
282
|
+
throw new Error(`The output folder has a ${MANIFEST_FILE} that this tool did not write, or that lists paths this tool never writes. `
|
|
283
|
+
+ `Nothing was changed. Use an empty folder, a folder from a previous extraction, or remove that file: ${manifest.target}`);
|
|
284
|
+
}
|
|
285
|
+
return scripts;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* 切換失敗時倒回去:收掉放上去的新檔,舊檔搬回原位。回傳搬不回去的原始路徑。
|
|
289
|
+
*
|
|
290
|
+
* 搬回去之前確認原位是空的;被別的東西占走就不搬(不覆蓋),那個檔留在暫存目錄裡。
|
|
291
|
+
*/
|
|
292
|
+
async function rollback(journal) {
|
|
293
|
+
for (const target of [...journal.placed].reverse()) {
|
|
294
|
+
await unlink(target).catch(() => undefined);
|
|
295
|
+
}
|
|
296
|
+
const stranded = [];
|
|
297
|
+
for (const { from, to } of [...journal.moved].reverse()) {
|
|
298
|
+
try {
|
|
299
|
+
const occupied = await lstat(from).then(() => true, (error) => {
|
|
300
|
+
if (code(error) === 'ENOENT')
|
|
301
|
+
return false;
|
|
302
|
+
throw error;
|
|
303
|
+
});
|
|
304
|
+
if (occupied) {
|
|
305
|
+
stranded.push(from);
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
// 不補建資料夾:原位的資料夾在切換時沒被動過,不在了就是別人動的,不跟著建。
|
|
309
|
+
await rename(to, from);
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
stranded.push(from);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return stranded;
|
|
316
|
+
}
|
|
317
|
+
/** 由深到淺收掉變空的資料夾,到輸出目錄為止。 */
|
|
318
|
+
async function removeEmptyDirs(root, starts) {
|
|
319
|
+
const dirs = new Set();
|
|
320
|
+
for (const start of starts) {
|
|
321
|
+
for (let dir = start; dir !== root && isPathInside(root, dir); dir = dirname(dir))
|
|
322
|
+
dirs.add(dir);
|
|
323
|
+
}
|
|
324
|
+
for (const dir of [...dirs].sort((a, b) => b.length - a.length)) {
|
|
325
|
+
await rmdir(dir).catch(() => undefined);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
//# sourceMappingURL=fs-snapshot-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fs-snapshot-store.js","sourceRoot":"","sources":["../../src/adapters/fs-snapshot-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -45,6 +45,8 @@ export declare class McpStudioBridge implements StudioBridge {
|
|
|
45
45
|
readonly readyTimeoutMs?: number;
|
|
46
46
|
/** 測試用;正式執行時啟動 StudioMCP 子行程。 */
|
|
47
47
|
readonly connect?: BridgeConnector;
|
|
48
|
+
/** 對 StudioMCP 自報的 client 版本,跟著本 server 的版本走。 */
|
|
49
|
+
readonly version?: string;
|
|
48
50
|
});
|
|
49
51
|
listStudios(): Promise<readonly ConnectedStudio[]>;
|
|
50
52
|
executeLuau(studioId: string, code: string, datamodel?: 'Edit' | 'Client' | 'Server'): Promise<string>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-studio-bridge.d.ts","sourceRoot":"","sources":["../../src/adapters/mcp-studio-bridge.ts"],"names":[],"mappings":"AAOA,OAAO,EAGL,KAAK,eAAe,EACpB,KAAK,YAAY,EAClB,MAAM,2BAA2B,CAAC;AAOnC;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAC3C;IAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,GAAG,SAAS,CA2B5E;AAaD,iCAAiC;AACjC,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,CACN,MAAM,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,EAC9E,YAAY,EAAE,SAAS,EACvB,OAAO,EAAE;QAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GACpC,OAAO,CAAC,OAAO,CAAC,CAAC;IACpB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM;IAAE,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"mcp-studio-bridge.d.ts","sourceRoot":"","sources":["../../src/adapters/mcp-studio-bridge.ts"],"names":[],"mappings":"AAOA,OAAO,EAGL,KAAK,eAAe,EACpB,KAAK,YAAY,EAClB,MAAM,2BAA2B,CAAC;AAOnC;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAC3C;IAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,GAAG,SAAS,CA2B5E;AAaD,iCAAiC;AACjC,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,CACN,MAAM,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,EAC9E,YAAY,EAAE,SAAS,EACvB,OAAO,EAAE;QAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GACpC,OAAO,CAAC,OAAO,CAAC,CAAC;IACpB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM;IAAE,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAAC;AA0BrG;;;;;;;;GAQG;AACH,qBAAa,eAAgB,YAAW,YAAY;;IAalD,YAAY,OAAO,GAAE;QACnB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAC1B,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;QAClC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAC5B,sCAAsC;QACtC,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;QACjC,iCAAiC;QACjC,QAAQ,CAAC,OAAO,CAAC,EAAE,eAAe,CAAC;QACnC,iDAAiD;QACjD,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;KACtB,EAOL;IA2DK,WAAW,IAAI,OAAO,CAAC,SAAS,eAAe,EAAE,CAAC,CAEvD;IAmBK,WAAW,CACf,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,SAAS,GAAE,MAAM,GAAG,QAAQ,GAAG,QAAiB,GAC/C,OAAO,CAAC,MAAM,CAAC,CAWjB;IAED,wCAAwC;IAClC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAQ3B;CAaF"}
|
|
@@ -52,7 +52,7 @@ function firstText(result, context) {
|
|
|
52
52
|
return text;
|
|
53
53
|
}
|
|
54
54
|
/** 正式執行時的連線方式:把 StudioMCP 當子行程啟動,以 MCP client 身分連上。 */
|
|
55
|
-
function stdioConnector(override) {
|
|
55
|
+
function stdioConnector(override, version) {
|
|
56
56
|
return () => {
|
|
57
57
|
const launch = override ?? resolveBridgeCommand();
|
|
58
58
|
if (launch === undefined) {
|
|
@@ -65,7 +65,7 @@ function stdioConnector(override) {
|
|
|
65
65
|
stderr: 'pipe',
|
|
66
66
|
env: getDefaultEnvironment(),
|
|
67
67
|
});
|
|
68
|
-
const client = new Client({ name: '
|
|
68
|
+
const client = new Client({ name: 'studio-sidecar-bridge', version });
|
|
69
69
|
return { client, ready: client.connect(transport) };
|
|
70
70
|
};
|
|
71
71
|
}
|
|
@@ -91,7 +91,7 @@ export class McpStudioBridge {
|
|
|
91
91
|
#timeoutMs;
|
|
92
92
|
#readyTimeoutMs;
|
|
93
93
|
constructor(options = {}) {
|
|
94
|
-
this.#connect = options.connect ?? stdioConnector(options.command === undefined ? undefined : { command: options.command, args: options.args ?? [] });
|
|
94
|
+
this.#connect = options.connect ?? stdioConnector(options.command === undefined ? undefined : { command: options.command, args: options.args ?? [] }, options.version ?? '0.0.0');
|
|
95
95
|
this.#timeoutMs = options.timeoutMs ?? 180_000;
|
|
96
96
|
this.#readyTimeoutMs = options.readyTimeoutMs ?? 20_000;
|
|
97
97
|
}
|