@weppy/roblox-mcp 1.5.3 → 1.5.4

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.
@@ -6,14 +6,14 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Roblox MCP server and tools for AI-powered game development",
9
- "version": "1.5.3"
9
+ "version": "1.5.4"
10
10
  },
11
11
  "plugins": [
12
12
  {
13
13
  "name": "weppy-roblox-mcp",
14
14
  "source": "./plugins/weppy-roblox-mcp",
15
15
  "description": "MCP server for Roblox Studio integration - AI-powered game development with specialized agents and skills",
16
- "version": "1.5.3",
16
+ "version": "1.5.4",
17
17
  "author": {
18
18
  "name": "hope1026"
19
19
  },
package/CHANGELOG.md CHANGED
@@ -12,6 +12,13 @@ All notable changes to this project will be documented in this file.
12
12
 
13
13
 
14
14
 
15
+ ## [1.5.4] - 2026-03-16
16
+
17
+ ### Improved
18
+
19
+ - Enhanced build pipeline stability and plugin artifact verification
20
+ - Improved production build process with additional validation gates
21
+
15
22
  ## [1.5.3] - 2026-03-15
16
23
 
17
24
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weppy/roblox-mcp",
3
- "version": "1.5.3",
3
+ "version": "1.5.4",
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",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "weppy-roblox-mcp",
3
3
  "description": "MCP server for Roblox Studio integration - AI-powered game development with specialized agents and skills",
4
- "version": "1.5.3",
4
+ "version": "1.5.4",
5
5
  "author": {
6
6
  "name": "hope1026"
7
7
  },
@@ -114,7 +114,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
114
114
  `).filter(g=>g.length>0)}catch(h){if(h.code==="ENOENT"){r.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};r.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,r]of this.ctx.places.entries())if(r.state==="syncing")return{active:!0,placeId:e};return{active:!1}}getDirectionForCategory(e){let r;if(this.ctx.activeFullSyncPlaceId!==null&&this.ctx.activeFullSyncPlaceId!==void 0&&(r=this.ctx.places.get(this.ctx.activeFullSyncPlaceId)),!r){let i=this.ctx.getDefaultRuntimePlaceId();i!=null&&(r=this.ctx.places.get(i))}if(!r)return"forward";let n=e;return r.directions[n]??"forward"}getStatusDirect(e){let r=e!=null?parseInt(e,10):this.ctx.getDefaultRuntimePlaceId();if(r==null)return{state:"idle",instanceCount:0,scriptCount:0,lastFullSync:null,lastIncrementalSync:null,syncRoot:this.ctx.config.getSyncRoot(),activeClientId:null,reverseSyncAvailable:!1,modifiedFileCount:0};let n=this.ctx.places.get(r);if(!n)return{state:"idle",instanceCount:0,scriptCount:0,lastFullSync:null,lastIncrementalSync:null,syncRoot:this.ctx.config.getPlaceRoot(r),activeClientId:null,reverseSyncAvailable:!1,modifiedFileCount:0};let i=n.fileWatcher?.getPendingCount()??0;return{state:n.state,instanceCount:n.instanceCount,scriptCount:n.scriptCount,lastFullSync:n.lastFullSync,lastIncrementalSync:n.lastIncrementalSync,syncRoot:this.ctx.config.getPlaceRoot(r),activeClientId:n.activeClientId,reverseSyncAvailable:i>0,modifiedFileCount:i}}getConfigDirect(){return this.ctx.config.getConfig()}async getHistoryDirect(e,r){let n=parseInt(e,10),i=this.ctx.places.get(n);i&&await i.writer.flushHistory();let a=Math.min(Math.max(r?.limit??50,1),200),o=Math.max(r?.offset??0,0),s=this.ctx.config.getHistoryPath(n),c=[];try{c=(await Ja.readFile(s,"utf-8")).split(`
115
115
  `).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 r=e!=null?parseInt(e,10):this.ctx.getDefaultRuntimePlaceId();if(r==null)return{...vi};let n=this.ctx.places.get(r);return n?{...n.directions}:{...vi}}getProgressDirect(e){let r=e!=null?parseInt(e,10):this.ctx.getDefaultRuntimePlaceId();if(r==null)return{state:"idle",isSyncing:!1};let n=this.ctx.places.get(r);if(!n)return{state:"idle",isSyncing:!1};if(!n.syncProgress)return{state:n.state,isSyncing:!1,lastSync:{instanceCount:n.instanceCount,scriptCount:n.scriptCount,completedAt:n.lastFullSync}};let i=n.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:n.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,r){let n=parseInt(e,10),i=this.ctx.places.get(n);if(!i)throw new Error(`Place ${e} not found in sync cache`);let a=i.index.resolvePropsPath(r);try{return{content:await Ja.readFile(a,"utf-8"),path:a}}catch(s){if(s.code!=="ENOENT")throw s}for(let s of Ua){let c=i.index.resolveScriptPath(r,s,!1);try{return{content:await Ja.readFile(c,"utf-8"),path:c}}catch{continue}}let o=i.index.resolveValuePath(r);try{return{content:await Ja.readFile(o,"utf-8"),path:o}}catch(s){if(s.code!=="ENOENT")throw s}throw new Error(`No synced file found for instance: ${r}`)}async writeSyncedFile(e,r,n){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(r,"Script",n,!1)}async executeViaDisk(e,r){let n=this.ctx.getDefaultRuntimePlaceId();if(n==null)throw new Error("No active sync place for disk execution");let i=this.ctx.places.get(n);if(!i)throw new Error(`Place ${n} not found in sync cache`);switch(e){case"set_script_source":{let a=r.scriptType||r.className||"Script";return await i.writer.writeScript(r.path,a,r.source,!1),{success:!0,path:r.path}}case"set_property":return await i.writer.writeProps(r.path,{className:r.className||"Instance",name:wt(r.path),properties:{[r.property]:r.value}}),{success:!0,path:r.path};default:throw new Error(`Disk execution not supported for action: ${e}`)}}};import $e from"path";import{randomUUID as PJ}from"crypto";import{promises as qt}from"fs";var xd=class{constructor(e){this.ctx=e}preserveLocalFilesMap=new Map;pendingServiceTrees=new Map;async handleInitStart(e,r,n){if(r.previousPlaceId!==void 0&&r.previousPlaceId!==e&&(v.info("Place promotion detected",{from:r.previousPlaceId,to:e}),await this.ctx.config.promotePlaceRoot(r.previousPlaceId,e,r.placeName),this.ctx.places.get(r.previousPlaceId)&&this.ctx.places.delete(r.previousPlaceId),this.ctx.activeFullSyncPlaceId===r.previousPlaceId&&(this.ctx.activeFullSyncPlaceId=null),this.ctx.clearRuntimePlaceIfMatch(r.previousPlaceId)),this.ctx.activeFullSyncPlaceId!==null&&this.ctx.activeFullSyncPlaceId!==void 0&&this.ctx.activeFullSyncPlaceId!==e){n.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,r.placeName);if(i.activeClientId&&i.activeClientId!==r.clientId&&i.activeFullSyncSessionId!==null){n.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=r.clientId,i.placeName=r.placeName,r.directions&&typeof r.directions=="object"){let p=r.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),v.info("Sync directions received",{placeId:e,directions:i.directions})}else i.directions={...vi};i.forwardRestoreQueue=[];let a=PJ();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 qt.mkdir(s,{recursive:!0}),i.tmpIndex=new Dn(o,s),i.tmpWriter=new qa(this.ctx.config,i.tmpIndex,e),i.collisionDirMap=new Map;let c=r.preserveLocalFiles;Array.isArray(c)&&c.length>0&&(this.setPreserveLocalFiles(a,c),v.info("PreserveLocalFiles set for sync",{syncId:a,fileCount:c.length}));let l={version:1,placeId:r.placeId,placeName:r.placeName,lastFullSync:null,lastIncrementalSync:null,instanceCount:0,scriptCount:0,syncMode:"mirror",propertyMode:this.ctx.config.getPropertyMode()},u=$e.join(o,".sync-meta.json");await qt.mkdir($e.dirname(u),{recursive:!0}),await this.ctx.atomicWriteFile(u,JSON.stringify(l,null,2)+`
116
116
  `),this.startTTLTimerForPlace(i,a),i.state="initializing",i.index.resetNameCounters(),i.syncProgress={syncStartTime:Date.now(),totalInstances:r.totalInstances,totalServices:r.totalServices,processedInstances:0,processedServices:0,currentService:null,currentChunkIndex:0,currentTotalChunks:0,bytesReceived:0,processedChunks:0},i.writer.appendChangeLog(`FULL_SYNC_START clientId=${r.clientId} placeId=${r.placeId}`),i.writer.appendHistory({timestamp:new Date().toISOString(),type:"fullSyncStart",direction:"forward",path:`place_${r.placeId}`,details:`services:${r.totalServices} instances:${r.totalInstances}`}),v.info("Full sync started",{syncId:a,clientId:r.clientId,placeId:r.placeId,placeName:r.placeName,totalServices:r.totalServices,totalInstances:r.totalInstances}),n.status(200).json({status:"started",syncId:i.activeFullSyncSessionId})}async handleInitChunk(e,r,n){let i=this.ctx.places.get(e);if(!i||!i.activeFullSyncSessionId||!i.tmpIndex||!i.tmpWriter){n.status(400).json({error:"No active sync session",message:"Call sync/init with phase=start first"});return}if(i.activeClientId&&r.clientId&&i.activeClientId!==r.clientId){n.status(409).json({error:"Conflict",message:`This sync session belongs to client ${i.activeClientId}`});return}let a=i.activeFullSyncSessionId,o=this.getOrCreatePendingServiceTree(a,r),s=0,c=i.collisionDirMap;for(let u of r.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 On(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 b=i.tmpIndex.sanitizeName(u.name),w=u;if(d!==p||f!==b){let k=i.tmpIndex.getExplorerRoot(),$=$e.relative(k,d).split($e.sep).filter(A=>A.length>0),N=We(["game",...$,f]);w={...u,path:N}}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,r.chunkIndex,r.totalChunks);if(l){let u=this.buildServiceTree(i,o);await i.tmpWriter.writeTree(r.serviceName,u);let p=this.pendingServiceTrees.get(a);p?.delete(r.serviceName),p&&p.size===0&&this.pendingServiceTrees.delete(a)}i.syncProgress&&(i.syncProgress.processedInstances+=s,i.syncProgress.currentService=r.serviceName,i.syncProgress.currentChunkIndex=r.chunkIndex,i.syncProgress.currentTotalChunks=r.totalChunks,i.syncProgress.processedChunks++,i.syncProgress.bytesReceived+=JSON.stringify(r).length,l&&i.syncProgress.processedServices++),v.debug("Sync chunk processed",{placeId:e,serviceName:r.serviceName,chunkIndex:r.chunkIndex,totalChunks:r.totalChunks,processed:s}),n.status(200).json({processed:s,service:r.serviceName})}async handleInitComplete(e,r,n){let i=this.ctx.places.get(e);if(!i||!i.activeFullSyncSessionId){n.status(400).json({error:"No active sync session",message:"Call sync/init with phase=start first"});return}if(i.activeClientId&&r.clientId&&i.activeClientId!==r.clientId){n.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 qt.readFile(h,"utf-8");u.set(m,g)}catch{p.push(m)}}v.info("Backed up local files for preservation",{placeId:e,requested:l.length,backed:u.size,deleted:p.length})}try{await qt.rm(s,{recursive:!0,force:!0})}catch{}await qt.rename(c,s),i.instanceCount=r.instanceCount,i.scriptCount=r.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",propertyMode:this.ctx.config.getPropertyMode()},f=$e.join(o,".sync-meta.json");if(await this.ctx.atomicWriteFile(f,JSON.stringify(d,null,2)+`
117
- `),this.clearTTLTimerForPlace(i,a),i.index.clearAllHashes(),i.index.clearClassMappings(),i.tmpIndex){let m=i.tmpIndex.getExplorerRoot(),h=i.index.getExplorerRoot();for(let[g,b]of i.tmpIndex.getAllHashes()){let w=$e.relative(m,g),_=$e.resolve(h,w);i.index.updateHashByValue(_,b)}for(let[g,b]of i.tmpIndex.getAllFileHashes()){let w=$e.relative(m,g),_=$e.resolve(h,w);i.index.updateFileHashByValue(_,b)}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 qt.mkdir($e.dirname(g),{recursive:!0}),await qt.writeFile(g,h,"utf-8")}catch(b){v.warn("Failed to restore preserved file",{path:m,error:b instanceof Error?b.message:String(b)})}}v.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 qt.unlink(g),i.index.removeHash(g),m++}catch(b){b.code!=="ENOENT"&&v.warn("Failed to delete preserved-as-deleted file",{path:h,error:b instanceof Error?b.message:String(b)})}}m>0&&(await i.index.saveToDisk(),v.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}`}),v.info("Full sync completed",{placeId:e,instanceCount:i.instanceCount,scriptCount:i.scriptCount}),n.status(200).json({status:"completed",instanceCount:i.instanceCount,scriptCount:i.scriptCount,syncRoot:this.ctx.config.getPlaceRoot(e)})}setPreserveLocalFiles(e,r){this.preserveLocalFilesMap.set(e,r)}getAndClearPreserveLocalFiles(e){let r=this.preserveLocalFilesMap.get(e)||[];return this.preserveLocalFilesMap.delete(e),r}clearPreserveLocalFiles(e){this.preserveLocalFilesMap.delete(e)}clearPendingServiceTrees(e){this.pendingServiceTrees.delete(e)}startTTLTimerForPlace(e,r){let n=setTimeout(async()=>{v.warn("Incomplete sync TTL expired, cleaning up",{placeId:e.placeId,syncId:r});let i=this.ctx.config.getPlaceRoot(e.placeId),a=$e.join(i,`explorer_tmp_${r}`);try{await qt.rm(a,{recursive:!0,force:!0})}catch(o){v.error("Failed to clean up expired temp dir",o instanceof Error?o:new Error(String(o)))}e.incompleteSyncTimer=null,e.activeFullSyncSessionId===r&&(e.activeFullSyncSessionId=null,e.activeClientId=null,e.state="idle",e.tmpIndex=null,this.pendingServiceTrees.delete(r),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))},Nz);n&&typeof n=="object"&&"unref"in n&&n.unref(),e.incompleteSyncTimer=n}getOrCreatePendingServiceTree(e,r){let n=this.pendingServiceTrees.get(e);n||(n=new Map,this.pendingServiceTrees.set(e,n));let i=n.get(r.serviceName);if(i)return i;let a={serviceName:r.tree?.name??r.serviceName,serviceClassName:r.tree?.className??r.serviceClassName,zeroBasedChunkIndex:r.chunkIndex===0,instances:[]};return n.set(r.serviceName,a),a}isLastChunk(e,r,n){return n<=1?!0:e.zeroBasedChunkIndex?r>=n-1:r>=n}buildServiceTree(e,r){let n={name:r.serviceName,className:r.serviceClassName,childCount:0,children:[],syncedAt:new Date().toISOString()};for(let i of r.instances){let a=this.resolveEffectiveSegments(e,i.effectivePath),o=pt(i.originalPath);this.upsertTreeNode(n,a,o,i.className)}return this.recomputeTreeChildCounts(n),n.syncedAt=new Date().toISOString(),n}resolveEffectiveSegments(e,r){return e.tmpIndex?pt(r).map(n=>e.tmpIndex.sanitizeName(n)):pt(r)}rewritePendingEffectivePaths(e,r,n,i){let a=$e.relative(r,n).split($e.sep).filter(l=>l.length>0),o=$e.relative(r,i).split($e.sep).filter(l=>l.length>0);if(a.length===0||o.length===0)return;let s=We(["game",...a]),c=We(["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,r,n,i){if(r.length<=1)return;let a=e.children;for(let o=1;o<r.length;o++){let s=r[o],c=n[o],l=o===r.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 r=n=>{let i=n.children??[];n.children=i;for(let a of i)r(a);n.childCount=i.length};for(let n of e.children)r(n);e.childCount=e.children.length}clearTTLTimerForPlace(e,r){e.incompleteSyncTimer&&e.activeFullSyncSessionId===r&&(clearTimeout(e.incompleteSyncTimer),e.incompleteSyncTimer=null)}async cleanupStaleTempDirs(){let e=this.ctx.config.getSyncRoot();try{let r=await qt.readdir(e,{withFileTypes:!0});for(let n of r)if(n.isDirectory()){if(n.name.startsWith("explorer_tmp_")){let i=$e.join(e,n.name);v.warn("Removing stale temp directory from crashed sync",{dir:n.name});try{await qt.rm(i,{recursive:!0,force:!0})}catch(a){v.error(`Failed to remove stale temp dir: ${n.name}`,a instanceof Error?a:new Error(String(a)))}}if(n.name.startsWith("place_")){let i=$e.join(e,n.name);try{let a=await qt.readdir(i,{withFileTypes:!0});for(let o of a)if(o.isDirectory()&&o.name.startsWith("explorer_tmp_")){let s=$e.join(i,o.name);v.warn("Removing stale temp directory from crashed sync",{dir:`${n.name}/${o.name}`});try{await qt.rm(s,{recursive:!0,force:!0})}catch(c){v.error(`Failed to remove stale temp dir: ${n.name}/${o.name}`,c instanceof Error?c:new Error(String(c)))}}}catch{continue}}}}catch(r){if(r.code==="ENOENT")return;v.warn("Failed to scan for stale temp dirs",{error:r instanceof Error?r.message:String(r)})}}};import jr from"path";import{promises as qz}from"fs";var bd=class{constructor(e){this.ctx=e}async handleReversePending(e,r){let n=this.ctx.resolveQueryPlaceId(e);if(n==null){r.status(200).json({pending:0,hasConflicts:!1,lastDetected:null});return}let i=this.ctx.places.get(n);if(!i){r.status(404).json({error:"Place not found",message:`No sync context for place ${n}`});return}let a={pending:i.fileWatcher?.getPendingCount()??0,hasConflicts:!1,lastDetected:i.fileWatcher?.getLastDetected()??null,forwardRestoreNeeded:i.forwardRestoreQueue.length};this.ctx.touchRuntimePlace(n),r.status(200).json(a)}async handleReverseSyncChanges(e,r){let n=this.ctx.resolveQueryPlaceId(e);if(n==null){r.status(200).json({changes:[],count:0});return}let i=this.ctx.places.get(n);if(!i){r.status(404).json({error:"Place not found",message:`No sync context for place ${n}`});return}if(i.state!=="syncing"){r.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(n),r.status(200).json({changes:o,count:o.length})}async handleReverseSyncResult(e,r){let n=e.body,i=n.placeId??this.ctx.getDefaultRuntimePlaceId();if(i==null){r.status(400).json({error:"Validation error",message:"placeId is required (in body or via active sync session)"});return}let a=n.appliedFiles??n.appliedPaths;if(!a||!Array.isArray(a)){r.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){r.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=jr.resolve(s,u);if(!gd(s,p)){l.push({path:u,error:"Path is outside the place root"});continue}try{let d=await qz.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=wt(m),g=St(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}`}),r.status(200).json({updated:c,failed:l.length,errors:l})}async handleResolveConflict(e,r){let n=e.body;if(!n.fsPath||!n.resolution){r.status(400).json({error:"Validation error",message:"fsPath and resolution are required"});return}let{fsPath:i,resolution:a}=n,o=this.ctx.config.getSyncRoot();if(!gd(o,jr.resolve(o,i))){r.status(403).json({error:"Forbidden",message:"Path is outside the sync root"});return}if(a==="skip"){r.status(200).json({status:"skipped",fsPath:i});return}let s;if(n.placeId&&(s=this.ctx.places.get(n.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){r.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=jr.resolve(c,i);if(!gd(c,l)){r.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(),r.status(200).json({status:"resolved",resolution:"apply-studio",fsPath:i});return}if(a==="apply-file"){let d=await qz.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);r.status(200).json({status:"resolved",resolution:"apply-file",fsPath:i,instancePath:h,fileType:m,content:d});return}r.status(400).json({error:"Invalid resolution",message:`Unknown resolution: ${a}`})}async handleReverseRescan(e,r){let n=this.ctx.resolveQueryPlaceId(e);if(n==null){r.status(200).json({added:0});return}let i=this.ctx.places.get(n);if(!i){r.status(404).json({error:"Place not found",message:`No sync context for place ${n}`});return}if(!i.fileWatcher){r.status(200).json({added:0});return}let a=await i.fileWatcher.rescan();this.ctx.touchRuntimePlace(n),v.info("Reverse rescan completed",{placeId:n,added:a}),r.status(200).json({added:a})}resolveInstancePathForAppliedPath(e,r){let n=e.resolveInstancePathFromFsPath(r);if(n)return n;let i=e.getExplorerRoot(),a=jr.relative(i,r);if(a.startsWith("..")||a===""||jr.isAbsolute(a))return null;let o=a.split(jr.sep).filter(f=>f.length>0);if(o.length<2)return null;let s=jr.basename(r),c=jr.dirname(r),l=e.getOriginalInstance(c,s);if(l)return l.instancePath;let u=s.toLowerCase();if(Ma.some(f=>u.endsWith(f))||u==="_tree.json")return null;let p=["game"],d=i;for(let f of o){d=jr.join(d,f);let m=jr.dirname(d);p.push(e.getOriginalNameForDir(m,f))}return We(p)}};function Zz(t){if(!t||typeof t!="object")return;let e=t;if(Array.isArray(e.instances))for(let r of e.instances)Array.isArray(r.properties)&&r.properties.length===0&&(r.properties={}),Array.isArray(r.attributes)&&r.attributes.length===0&&(r.attributes={});if(Array.isArray(e.changes))for(let r of e.changes)Array.isArray(r.properties)&&r.properties.length===0&&(r.properties={}),Array.isArray(r.attributes)&&r.attributes.length===0&&(r.attributes={})}var _d=class{config;places;apiHandler;changeProcessor;initHandler;reverseHandler;activeFullSyncPlaceId=null;activeRuntimeSyncPlaceId=null;constructor(e){this.config=new ed(e),this.apiHandler=new yd(this),this.changeProcessor=new vd(this),this.initHandler=new xd(this),this.reverseHandler=new bd(this),this.places=new Yp({max:3,dispose:(r,n)=>{v.info("Disposing place context (LRU eviction)",{placeId:n}),this.activeFullSyncPlaceId===n&&(this.activeFullSyncPlaceId=null),this.activeRuntimeSyncPlaceId===n&&(this.activeRuntimeSyncPlaceId=null),r.fileWatcher&&(r.fileWatcher.stop().catch(i=>{v.error("Error stopping file watcher during dispose",i)}),r.fileWatcher=null),r.writer.stopChangeLogFlusher(),r.incompleteSyncTimer&&(clearTimeout(r.incompleteSyncTimer),r.incompleteSyncTimer=null),r.index.saveToDisk().catch(i=>{v.error("Error saving index during dispose",i)}),r.activeFullSyncSessionId&&this.initHandler.clearPendingServiceTrees(r.activeFullSyncSessionId),r.tmpWriter&&(r.tmpWriter.stopChangeLogFlusher(),r.tmpWriter=null),r.tmpIndex=null,r.collisionDirMap=null}})}async getOrCreatePlaceContext(e,r){let n=this.places.get(e);if(n&&r){let i=this.config.getPlaceRoot(e),a=await this.config.resolvePlaceRoot(e,r);a!==i&&(v.info("Place root migrated, recreating context",{placeId:e,from:i,to:a}),this.places.delete(e),n=void 0)}if(!n){let i=await this.config.resolvePlaceRoot(e,r),a=y_.join(i,"explorer");await Ga.mkdir(i,{recursive:!0}),await Ga.mkdir(a,{recursive:!0});let o=new Dn(i,a);await o.loadFromDisk();let s=new qa(this.config,o,e),c=new nd(this.config,o,i);s.startChangeLogFlusher(),n={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:{...vi},forwardRestoreQueue:[],syncProgress:null,collisionDirMap:null},this.places.set(e,n),v.info("Created new place context",{placeId:e,placeRoot:i})}return n}async handleSyncInit(e,r){try{Zz(e.body);let n=Mz(e.body);if(!n.success){r.status(400).json({error:"Validation error",message:n.error});return}let i=n.data,a=i.phase==="start"?i.placeId??null:i.phase==="chunk"||i.phase==="complete"?this.activeFullSyncPlaceId:null;if(a==null){r.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,r);break;case"chunk":await this.initHandler.handleInitChunk(a,i,r);break;case"complete":await this.initHandler.handleInitComplete(a,i,r);break}}catch(n){this.sendError(r,n,"handleSyncInit")}}async handleSyncUpdate(e,r){try{Zz(e.body);let n=Uz(e.body);if(!n.success){r.status(400).json({error:"Validation error",message:n.error});return}let i=n.data,a=i.placeId??this.getDefaultRuntimePlaceId();if(a==null){r.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"){r.status(409).json({error:"Conflict",message:`Full sync in progress for place ${a}`});return}o.activeClientId=i.clientId,this.touchRuntimePlace(a);let s=Oz(o,i.changes);v.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 fd(o,f)}catch(m){c.push({path:f.instancePath,error:m instanceof Error?m.message:String(m)})}o.changesSinceLastSave+=u,o.changesSinceLastSave>=Az&&(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),r.status(200).json(d)}catch(n){this.sendError(r,n,"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,r]of this.places.entries())if(r.state==="syncing"||r.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,r="runtime"){let n=e.query.placeId;if(n){let i=parseInt(n,10);if(!isNaN(i))return i}return r==="full"?this.activeFullSyncPlaceId:this.getDefaultRuntimePlaceId()}async handleSyncStatus(e,r){try{let n=this.resolveQueryPlaceId(e);if(n==null){let s={state:"idle",instanceCount:0,scriptCount:0,lastFullSync:null,lastIncrementalSync:null,syncRoot:this.config.getSyncRoot(),activeClientId:null,reverseSyncAvailable:!1,modifiedFileCount:0};r.status(200).json(s);return}let i=this.places.get(n);if(!i){let s={state:"idle",instanceCount:0,scriptCount:0,lastFullSync:null,lastIncrementalSync:null,syncRoot:this.config.getPlaceRoot(n),activeClientId:null,reverseSyncAvailable:!1,modifiedFileCount:0};r.status(200).json(s);return}let a=i.fileWatcher?.getPendingCount()??0;this.touchRuntimePlace(n);let o={state:i.state,instanceCount:i.instanceCount,scriptCount:i.scriptCount,lastFullSync:i.lastFullSync,lastIncrementalSync:i.lastIncrementalSync,syncRoot:this.config.getPlaceRoot(n),activeClientId:i.activeClientId,reverseSyncAvailable:a>0,modifiedFileCount:a,directions:i.directions,fileWatcherActive:i.fileWatcher!==null,forwardOnlyClasses:[...Fa]};r.status(200).json(o)}catch(n){this.sendError(r,n,"handleSyncStatus")}}async handleSyncStop(e,r){try{let n=e.body,i=n.placeId??this.getDefaultRuntimePlaceId();if(i==null){r.status(200).json({status:"idle",state:"idle",placeId:null,message:"No active sync place"});return}let a=this.places.get(i);if(!a){r.status(200).json({status:"idle",state:"idle",placeId:i,message:`No sync context for place ${i}`});return}if(n.clientId&&a.activeClientId&&n.clientId!==a.activeClientId&&(a.activeFullSyncSessionId!==null||a.state==="syncing"||a.state==="initializing")){r.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=y_.join(s,`explorer_tmp_${o}`);await Ga.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),v.info("Sync stopped",{placeId:i,reason:n.reason??"requested"}),r.status(200).json({status:"stopped",state:"idle",placeId:i})}catch(n){this.sendError(r,n,"handleSyncStop")}}async handleSyncConfig(e,r){try{if(e.method==="GET"){r.status(200).json(this.config.getConfig());return}let n=Fz(e.body);if(!n.success){r.status(400).json({error:"Validation error",message:n.error});return}let i=n.data;i.propertyMode!==void 0&&this.config.setPropertyMode(i.propertyMode),i.maxDepth!==void 0&&this.config.updateConfig({maxDepth:i.maxDepth}),i.maxInstances!==void 0&&this.config.updateConfig({maxInstances:i.maxInstances}),r.status(200).json({status:"updated",config:this.config.getConfig()})}catch(n){this.sendError(r,n,"handleSyncConfig")}}async initialize(){try{await this.config.loadFromMeta(),await this.initHandler.cleanupStaleTempDirs(),v.info("SyncController initialized")}catch(e){v.error("SyncController initialization failed",e instanceof Error?e:new Error(String(e)))}}async shutdown(){try{this.places.clear(),v.info("SyncController shut down")}catch(e){v.error("SyncController shutdown error",e instanceof Error?e:new Error(String(e)))}}async atomicWriteFile(e,r){let n=e+".tmp."+EJ().slice(0,8);try{await Ga.writeFile(n,r,"utf-8"),await Ga.rename(n,e)}catch(i){throw await Ga.unlink(n).catch(()=>{}),i}}async handlePreCheck(e,r){try{await this.apiHandler.handlePreCheck(e,r)}catch(n){this.sendError(r,n,"handlePreCheck")}}async handleSyncDirections(e,r){try{await this.apiHandler.handleSyncDirections(e,r)}catch(n){this.sendError(r,n,"handleSyncDirections")}}async handleForwardRestoreList(e,r){try{await this.apiHandler.handleForwardRestoreList(e,r)}catch(n){this.sendError(r,n,"handleForwardRestoreList")}}async handleReversePending(e,r){try{await this.reverseHandler.handleReversePending(e,r)}catch(n){this.sendError(r,n,"handleReversePending")}}async handleReverseSyncChanges(e,r){try{await this.reverseHandler.handleReverseSyncChanges(e,r)}catch(n){this.sendError(r,n,"handleReverseSyncChanges")}}async handleReverseSyncResult(e,r){try{await this.reverseHandler.handleReverseSyncResult(e,r)}catch(n){this.sendError(r,n,"handleReverseSyncResult")}}async handleResolveConflict(e,r){try{await this.reverseHandler.handleResolveConflict(e,r)}catch(n){this.sendError(r,n,"handleResolveConflict")}}async handleReverseRescan(e,r){try{await this.reverseHandler.handleReverseRescan(e,r)}catch(n){this.sendError(r,n,"handleReverseRescan")}}async handleSyncHistory(e,r){try{await this.apiHandler.handleSyncHistory(e,r)}catch(n){this.sendError(r,n,"handleSyncHistory")}}async startFileWatcherForPlace(e){if(e.state!=="syncing"){v.debug("Skipping file watcher start - place not syncing",{placeId:e.placeId,state:e.state});return}e.fileWatcher&&await e.fileWatcher.stop();let r=y_.join(this.config.getPlaceRoot(e.placeId),"explorer");e.fileWatcher=new dd(r,e.index),e.writer.setOnWriteCallback(n=>{e.fileWatcher?.suppressPath(n)}),e.fileWatcher.setDirectionChecker(n=>{let i=az(n);return e.directions[i]}),e.fileWatcher.setOnForwardViolation(n=>{e.forwardRestoreQueue.includes(n)||(e.forwardRestoreQueue.push(n),v.info("Forward violation queued for restore",{placeId:e.placeId,relativePath:n,queueSize:e.forwardRestoreQueue.length}))}),await e.fileWatcher.start(),v.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,r){return this.apiHandler.getHistoryDirect(e,r)}getDirectionsDirect(e){return this.apiHandler.getDirectionsDirect(e)}getProgressDirect(e){return this.apiHandler.getProgressDirect(e)}async readSyncedFile(e,r){return this.apiHandler.readSyncedFile(e,r)}async writeSyncedFile(e,r,n){await this.apiHandler.writeSyncedFile(e,r,n)}async executeViaDisk(e,r){return this.apiHandler.executeViaDisk(e,r)}sendError(e,r,n){let i=r instanceof Error?r.message:String(r);if(i.includes("Path traversal detected")){e.status(403).json({error:"Forbidden",message:i});return}let a=r.code;if(a==="ENOSPC"||a==="EPERM"||a==="EACCES"){e.status(500).json({error:"Disk error",message:i});return}v.error(`SyncController.${n} failed`,r instanceof Error?r:new Error(i)),e.status(500).json({error:"Internal error",message:`${n}: ${i}`})}};import{randomUUID as TJ}from"crypto";function Hz(t,e){let r=TJ(),n=r.replace(/-/g,"").substring(0,8).toUpperCase(),i=`${n.substring(0,4)}-${n.substring(4,8)}`;return{config:t,app:e,instanceId:r,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,clientModeLastHealthSuccessAt:null,clientModeLastHealthFailureAt:null,clientModeLastHealthError:null,historyManager:null,analyticsManager:null,licenseState:null,syncController:null,internalActionExecutor:null,playtestControlCommand:null,aiClientName:"",pluginVersion:"",syncedSessionToken:null}}var Bz=ro(Jp(),1);function Vz(t){let e=Bz.default.json({limit:"5mb"});t.app.use((r,n,i)=>{if(r.path.startsWith("/sync/")){i();return}e(r,n,i)}),t.app.use((r,n,i)=>{n.setHeader("Access-Control-Allow-Origin","http://localhost:3002"),n.setHeader("Access-Control-Allow-Methods","GET, POST, OPTIONS"),n.setHeader("Access-Control-Allow-Headers","Content-Type"),i()}),t.app.use((r,n,i)=>{v.debug(`${r.method} ${r.path}`,{ip:r.ip}),i()})}import{randomUUID as Ec}from"crypto";function CJ(){let t=process.env.WEPPY_ROBLOX_MCP_VERSION?.trim();return t||null}var Je=CJ()??"1.5.3";function Wz(t){let e=new Set;t.aiClientName&&e.add(t.aiClientName);for(let r of t.mcpInstances.values())r.aiClientName&&e.add(r.aiClientName);return Array.from(e)}function RJ(t){let r=Date.now();return Array.from(t.pluginClients.values()).filter(n=>r-n.lastSeen<1e4)}function Jz(t){let r=Date.now();for(let[n,i]of t.mcpInstances)i.lastSeen&&r-i.lastSeen>15e3&&(t.mcpInstances.delete(n),v.debug("Removed stale MCP instance",{instanceId:n,lastSeen:i.lastSeen}))}function Gz(t,e,r){let n=e.query.clientId;if(n&&t.pluginClients.has(n)){let i=t.pluginClients.get(n);i.lastSeen=Date.now()}try{let i=e.body;if(!i||!Array.isArray(i.selection)||typeof i.count!="number"){v.warn("Invalid selection update request",{body:i}),r.status(400).json({error:"Invalid request body"});return}let a=n||"unknown",o=Date.now();t.cachedSelectionMap.set(a,{selection:i.selection,count:i.count,timestamp:o,clientId:a}),v.debug("Selection cache updated",{count:i.count,clientId:a,timestamp:o}),r.json({status:"ok",timestamp:o})}catch(i){v.error("Error handling selection update",i),r.status(500).json({error:"Internal server error"})}}function Kz(t,e,r){let n=parseInt(e.query.maxAge)||3e4,i=Pc(t,n);i?r.json({cached:!0,...i}):r.json({cached:!1,message:"No cached selection available"})}function Pc(t,e=3e4,r){if(t.cachedSelectionMap.size===0)return null;let n;if(r)n=t.cachedSelectionMap.get(r);else for(let a of t.cachedSelectionMap.values())(!n||a.timestamp>n.timestamp)&&(n=a);if(!n)return null;let i=Date.now()-n.timestamp;return e===0||i<=e?n:null}function Xz(t,e,r){try{let n=e.body;if(!n.clientId){r.status(400).json({error:"Missing clientId"});return}let i=t.pluginClients.get(n.clientId),a=Date.now(),o={clientId:n.clientId,projectName:n.projectName,placeName:n.placeName,placeId:n.placeId,connectedAt:i?.connectedAt||a,lastSeen:a,commandsProcessed:i?.commandsProcessed||0,connectionType:"polling"};t.pluginClients.set(n.clientId,o),t.pendingCommands.has(n.clientId)||t.pendingCommands.set(n.clientId,[]),n.pluginVersion&&(t.pluginVersion=n.pluginVersion),t.analyticsManager&&(n.pluginVersion&&t.analyticsManager.setPluginVersion(n.pluginVersion),t.analyticsManager.trackPluginConnected()),v.info("Plugin client registered",{clientId:n.clientId,projectName:n.projectName,placeName:n.placeName,isReconnect:!!i}),r.json({status:"ok",clientId:n.clientId,serverInstanceId:t.instanceId,sessionId:t.sessionId,mcpVersion:Je,connectedAt:o.connectedAt,aiClientNames:Wz(t),serverStartTime:t.startTime,mcpInstanceCount:t.mcpInstances.size+1})}catch(n){v.error("Error registering plugin client",n),r.status(500).json({error:"Internal server error"})}}function Yz(t,e,r){let n=e.body?.clientId;if(!n){r.status(400).json({error:"Missing clientId"});return}let i=t.pluginClients.has(n);t.pluginClients.delete(n),t.pendingCommands.delete(n),v.info("Plugin client unregistered",{clientId:n,existed:i}),r.json({status:"ok",existed:i})}function Qz(t,e,r){try{let n=e.body;if(!n.instanceId){r.status(400).json({error:"Missing instanceId"});return}let i=Date.now(),a={instanceId:n.instanceId,pid:n.pid,connectedAt:i,isServer:!1,lastSeen:i};n.aiClientName&&(a.aiClientName=n.aiClientName),n.cwd&&(a.cwd=n.cwd),t.mcpInstances.set(n.instanceId,a),v.info("MCP instance registered (client mode)",{instanceId:n.instanceId,pid:n.pid,cwd:n.cwd}),r.json({status:"ok",instanceId:n.instanceId,serverInstanceId:t.instanceId,sessionId:t.sessionId,mcpVersion:Je,mcpInstanceCount:t.mcpInstances.size+1})}catch(n){v.error("Error registering MCP instance",n),r.status(500).json({error:"Internal server error"})}}function ej(t,e,r){let n=e.body?.instanceId;if(!n){r.status(400).json({error:"Missing instanceId"});return}let i=t.mcpInstances.has(n);t.mcpInstances.delete(n),v.info("MCP instance unregistered",{instanceId:n,existed:i}),r.json({status:"ok",existed:i})}function tj(t,e,r){let{instanceId:n,aiClientName:i}=e.body;if(n&&i){let a=t.mcpInstances.get(n);a&&(a.aiClientName=i)}r.json({status:"ok"})}function rj(t){let r=Date.now();for(let[n,i]of t.pluginClients)r-i.lastSeen>3e4&&(t.pluginClients.delete(n),t.pendingCommands.delete(n));return Jz(t),{serverInstanceId:t.instanceId,sessionId:t.sessionId,mcpVersion:Je,uptime:r-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()},...Array.from(t.mcpInstances.values())],mcpInstanceCount:t.mcpInstances.size+1}}function nj(t,e){e.json(rj(t))}function ij(t,e,r){let n=e.query.instanceId;n&&t.mcpInstances.has(n)&&(t.mcpInstances.get(n).lastSeen=Date.now()),Jz(t);let i=RJ(t),a=t.sseClients.size,c={...{status:"online",connectedClients:a+i.length,queuedCommands:t.commandQueue.size,uptime:Date.now()-t.startTime,version:Je,isClientMode:t.isClientMode,pid:process.pid,sessionId:t.sessionId},instanceId:t.instanceId,mcpInstanceCount:t.mcpInstances.size+1,aiClientNames:Wz(t),pluginVersion:t.pluginVersion||void 0,sseClients:a,pollingClients:i.length,pluginClients:i.map(l=>({clientId:l.clientId,projectName:l.projectName,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}}:{}};r.json(c)}function aj(t,e,r,n){let i=e.ip||e.socket.remoteAddress||"";if(!(i==="127.0.0.1"||i==="::1"||i==="::ffff:127.0.0.1"||i==="localhost")){v.warn("Shutdown request rejected from non-localhost",{ip:i}),r.status(403).json({error:"Forbidden: localhost only"});return}v.info("Shutdown request received, initiating graceful shutdown",{requestedBy:i,uptime:Date.now()-t.startTime}),r.json({status:"shutting_down",message:"Server will shutdown gracefully",pid:process.pid}),setTimeout(async()=>{try{await n(),v.info("Graceful shutdown completed"),process.exit(0)}catch(o){v.error("Error during graceful shutdown",o),process.exit(1)}},100)}async function wd(t){if(t.isClientMode)try{let e=await fetch(`${t.baseUrl}/connection-info`);if(e.ok)return await e.json()}catch(e){v.warn("Failed to fetch connection info from server",{error:e})}return rj(t)}var zJ={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 Sd(t){return zJ[t]||"plugin"}var kd=100;function oj(t,e,r){v.info("Plugin connected via SSE"),r.setHeader("Content-Type","text/event-stream"),r.setHeader("Cache-Control","no-cache"),r.setHeader("Connection","keep-alive"),t.sseClients.add(r),x_(r,{event:"command",id:Ec(),data:{action:"connected",requestId:Ec(),params:{serverVersion:Je,timestamp:Date.now()}}});let n=setInterval(()=>{x_(r,{event:"command",id:Ec(),data:{action:"keepalive",requestId:Ec(),params:{timestamp:Date.now()}}})},3e4);r.on("close",()=>{v.info("Plugin disconnected from SSE"),clearInterval(n),t.sseClients.delete(r)})}function x_(t,e){let r=JSON.stringify(e.data);t.write(`event: ${e.event}
117
+ `),this.clearTTLTimerForPlace(i,a),i.index.clearAllHashes(),i.index.clearClassMappings(),i.tmpIndex){let m=i.tmpIndex.getExplorerRoot(),h=i.index.getExplorerRoot();for(let[g,b]of i.tmpIndex.getAllHashes()){let w=$e.relative(m,g),_=$e.resolve(h,w);i.index.updateHashByValue(_,b)}for(let[g,b]of i.tmpIndex.getAllFileHashes()){let w=$e.relative(m,g),_=$e.resolve(h,w);i.index.updateFileHashByValue(_,b)}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 qt.mkdir($e.dirname(g),{recursive:!0}),await qt.writeFile(g,h,"utf-8")}catch(b){v.warn("Failed to restore preserved file",{path:m,error:b instanceof Error?b.message:String(b)})}}v.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 qt.unlink(g),i.index.removeHash(g),m++}catch(b){b.code!=="ENOENT"&&v.warn("Failed to delete preserved-as-deleted file",{path:h,error:b instanceof Error?b.message:String(b)})}}m>0&&(await i.index.saveToDisk(),v.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}`}),v.info("Full sync completed",{placeId:e,instanceCount:i.instanceCount,scriptCount:i.scriptCount}),n.status(200).json({status:"completed",instanceCount:i.instanceCount,scriptCount:i.scriptCount,syncRoot:this.ctx.config.getPlaceRoot(e)})}setPreserveLocalFiles(e,r){this.preserveLocalFilesMap.set(e,r)}getAndClearPreserveLocalFiles(e){let r=this.preserveLocalFilesMap.get(e)||[];return this.preserveLocalFilesMap.delete(e),r}clearPreserveLocalFiles(e){this.preserveLocalFilesMap.delete(e)}clearPendingServiceTrees(e){this.pendingServiceTrees.delete(e)}startTTLTimerForPlace(e,r){let n=setTimeout(async()=>{v.warn("Incomplete sync TTL expired, cleaning up",{placeId:e.placeId,syncId:r});let i=this.ctx.config.getPlaceRoot(e.placeId),a=$e.join(i,`explorer_tmp_${r}`);try{await qt.rm(a,{recursive:!0,force:!0})}catch(o){v.error("Failed to clean up expired temp dir",o instanceof Error?o:new Error(String(o)))}e.incompleteSyncTimer=null,e.activeFullSyncSessionId===r&&(e.activeFullSyncSessionId=null,e.activeClientId=null,e.state="idle",e.tmpIndex=null,this.pendingServiceTrees.delete(r),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))},Nz);n&&typeof n=="object"&&"unref"in n&&n.unref(),e.incompleteSyncTimer=n}getOrCreatePendingServiceTree(e,r){let n=this.pendingServiceTrees.get(e);n||(n=new Map,this.pendingServiceTrees.set(e,n));let i=n.get(r.serviceName);if(i)return i;let a={serviceName:r.tree?.name??r.serviceName,serviceClassName:r.tree?.className??r.serviceClassName,zeroBasedChunkIndex:r.chunkIndex===0,instances:[]};return n.set(r.serviceName,a),a}isLastChunk(e,r,n){return n<=1?!0:e.zeroBasedChunkIndex?r>=n-1:r>=n}buildServiceTree(e,r){let n={name:r.serviceName,className:r.serviceClassName,childCount:0,children:[],syncedAt:new Date().toISOString()};for(let i of r.instances){let a=this.resolveEffectiveSegments(e,i.effectivePath),o=pt(i.originalPath);this.upsertTreeNode(n,a,o,i.className)}return this.recomputeTreeChildCounts(n),n.syncedAt=new Date().toISOString(),n}resolveEffectiveSegments(e,r){return e.tmpIndex?pt(r).map(n=>e.tmpIndex.sanitizeName(n)):pt(r)}rewritePendingEffectivePaths(e,r,n,i){let a=$e.relative(r,n).split($e.sep).filter(l=>l.length>0),o=$e.relative(r,i).split($e.sep).filter(l=>l.length>0);if(a.length===0||o.length===0)return;let s=We(["game",...a]),c=We(["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,r,n,i){if(r.length<=1)return;let a=e.children;for(let o=1;o<r.length;o++){let s=r[o],c=n[o],l=o===r.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 r=n=>{let i=n.children??[];n.children=i;for(let a of i)r(a);n.childCount=i.length};for(let n of e.children)r(n);e.childCount=e.children.length}clearTTLTimerForPlace(e,r){e.incompleteSyncTimer&&e.activeFullSyncSessionId===r&&(clearTimeout(e.incompleteSyncTimer),e.incompleteSyncTimer=null)}async cleanupStaleTempDirs(){let e=this.ctx.config.getSyncRoot();try{let r=await qt.readdir(e,{withFileTypes:!0});for(let n of r)if(n.isDirectory()){if(n.name.startsWith("explorer_tmp_")){let i=$e.join(e,n.name);v.warn("Removing stale temp directory from crashed sync",{dir:n.name});try{await qt.rm(i,{recursive:!0,force:!0})}catch(a){v.error(`Failed to remove stale temp dir: ${n.name}`,a instanceof Error?a:new Error(String(a)))}}if(n.name.startsWith("place_")){let i=$e.join(e,n.name);try{let a=await qt.readdir(i,{withFileTypes:!0});for(let o of a)if(o.isDirectory()&&o.name.startsWith("explorer_tmp_")){let s=$e.join(i,o.name);v.warn("Removing stale temp directory from crashed sync",{dir:`${n.name}/${o.name}`});try{await qt.rm(s,{recursive:!0,force:!0})}catch(c){v.error(`Failed to remove stale temp dir: ${n.name}/${o.name}`,c instanceof Error?c:new Error(String(c)))}}}catch{continue}}}}catch(r){if(r.code==="ENOENT")return;v.warn("Failed to scan for stale temp dirs",{error:r instanceof Error?r.message:String(r)})}}};import jr from"path";import{promises as qz}from"fs";var bd=class{constructor(e){this.ctx=e}async handleReversePending(e,r){let n=this.ctx.resolveQueryPlaceId(e);if(n==null){r.status(200).json({pending:0,hasConflicts:!1,lastDetected:null});return}let i=this.ctx.places.get(n);if(!i){r.status(404).json({error:"Place not found",message:`No sync context for place ${n}`});return}let a={pending:i.fileWatcher?.getPendingCount()??0,hasConflicts:!1,lastDetected:i.fileWatcher?.getLastDetected()??null,forwardRestoreNeeded:i.forwardRestoreQueue.length};this.ctx.touchRuntimePlace(n),r.status(200).json(a)}async handleReverseSyncChanges(e,r){let n=this.ctx.resolveQueryPlaceId(e);if(n==null){r.status(200).json({changes:[],count:0});return}let i=this.ctx.places.get(n);if(!i){r.status(404).json({error:"Place not found",message:`No sync context for place ${n}`});return}if(i.state!=="syncing"){r.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(n),r.status(200).json({changes:o,count:o.length})}async handleReverseSyncResult(e,r){let n=e.body,i=n.placeId??this.ctx.getDefaultRuntimePlaceId();if(i==null){r.status(400).json({error:"Validation error",message:"placeId is required (in body or via active sync session)"});return}let a=n.appliedFiles??n.appliedPaths;if(!a||!Array.isArray(a)){r.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){r.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=jr.resolve(s,u);if(!gd(s,p)){l.push({path:u,error:"Path is outside the place root"});continue}try{let d=await qz.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=wt(m),g=St(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}`}),r.status(200).json({updated:c,failed:l.length,errors:l})}async handleResolveConflict(e,r){let n=e.body;if(!n.fsPath||!n.resolution){r.status(400).json({error:"Validation error",message:"fsPath and resolution are required"});return}let{fsPath:i,resolution:a}=n,o=this.ctx.config.getSyncRoot();if(!gd(o,jr.resolve(o,i))){r.status(403).json({error:"Forbidden",message:"Path is outside the sync root"});return}if(a==="skip"){r.status(200).json({status:"skipped",fsPath:i});return}let s;if(n.placeId&&(s=this.ctx.places.get(n.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){r.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=jr.resolve(c,i);if(!gd(c,l)){r.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(),r.status(200).json({status:"resolved",resolution:"apply-studio",fsPath:i});return}if(a==="apply-file"){let d=await qz.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);r.status(200).json({status:"resolved",resolution:"apply-file",fsPath:i,instancePath:h,fileType:m,content:d});return}r.status(400).json({error:"Invalid resolution",message:`Unknown resolution: ${a}`})}async handleReverseRescan(e,r){let n=this.ctx.resolveQueryPlaceId(e);if(n==null){r.status(200).json({added:0});return}let i=this.ctx.places.get(n);if(!i){r.status(404).json({error:"Place not found",message:`No sync context for place ${n}`});return}if(!i.fileWatcher){r.status(200).json({added:0});return}let a=await i.fileWatcher.rescan();this.ctx.touchRuntimePlace(n),v.info("Reverse rescan completed",{placeId:n,added:a}),r.status(200).json({added:a})}resolveInstancePathForAppliedPath(e,r){let n=e.resolveInstancePathFromFsPath(r);if(n)return n;let i=e.getExplorerRoot(),a=jr.relative(i,r);if(a.startsWith("..")||a===""||jr.isAbsolute(a))return null;let o=a.split(jr.sep).filter(f=>f.length>0);if(o.length<2)return null;let s=jr.basename(r),c=jr.dirname(r),l=e.getOriginalInstance(c,s);if(l)return l.instancePath;let u=s.toLowerCase();if(Ma.some(f=>u.endsWith(f))||u==="_tree.json")return null;let p=["game"],d=i;for(let f of o){d=jr.join(d,f);let m=jr.dirname(d);p.push(e.getOriginalNameForDir(m,f))}return We(p)}};function Zz(t){if(!t||typeof t!="object")return;let e=t;if(Array.isArray(e.instances))for(let r of e.instances)Array.isArray(r.properties)&&r.properties.length===0&&(r.properties={}),Array.isArray(r.attributes)&&r.attributes.length===0&&(r.attributes={});if(Array.isArray(e.changes))for(let r of e.changes)Array.isArray(r.properties)&&r.properties.length===0&&(r.properties={}),Array.isArray(r.attributes)&&r.attributes.length===0&&(r.attributes={})}var _d=class{config;places;apiHandler;changeProcessor;initHandler;reverseHandler;activeFullSyncPlaceId=null;activeRuntimeSyncPlaceId=null;constructor(e){this.config=new ed(e),this.apiHandler=new yd(this),this.changeProcessor=new vd(this),this.initHandler=new xd(this),this.reverseHandler=new bd(this),this.places=new Yp({max:3,dispose:(r,n)=>{v.info("Disposing place context (LRU eviction)",{placeId:n}),this.activeFullSyncPlaceId===n&&(this.activeFullSyncPlaceId=null),this.activeRuntimeSyncPlaceId===n&&(this.activeRuntimeSyncPlaceId=null),r.fileWatcher&&(r.fileWatcher.stop().catch(i=>{v.error("Error stopping file watcher during dispose",i)}),r.fileWatcher=null),r.writer.stopChangeLogFlusher(),r.incompleteSyncTimer&&(clearTimeout(r.incompleteSyncTimer),r.incompleteSyncTimer=null),r.index.saveToDisk().catch(i=>{v.error("Error saving index during dispose",i)}),r.activeFullSyncSessionId&&this.initHandler.clearPendingServiceTrees(r.activeFullSyncSessionId),r.tmpWriter&&(r.tmpWriter.stopChangeLogFlusher(),r.tmpWriter=null),r.tmpIndex=null,r.collisionDirMap=null}})}async getOrCreatePlaceContext(e,r){let n=this.places.get(e);if(n&&r){let i=this.config.getPlaceRoot(e),a=await this.config.resolvePlaceRoot(e,r);a!==i&&(v.info("Place root migrated, recreating context",{placeId:e,from:i,to:a}),this.places.delete(e),n=void 0)}if(!n){let i=await this.config.resolvePlaceRoot(e,r),a=y_.join(i,"explorer");await Ga.mkdir(i,{recursive:!0}),await Ga.mkdir(a,{recursive:!0});let o=new Dn(i,a);await o.loadFromDisk();let s=new qa(this.config,o,e),c=new nd(this.config,o,i);s.startChangeLogFlusher(),n={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:{...vi},forwardRestoreQueue:[],syncProgress:null,collisionDirMap:null},this.places.set(e,n),v.info("Created new place context",{placeId:e,placeRoot:i})}return n}async handleSyncInit(e,r){try{Zz(e.body);let n=Mz(e.body);if(!n.success){r.status(400).json({error:"Validation error",message:n.error});return}let i=n.data,a=i.phase==="start"?i.placeId??null:i.phase==="chunk"||i.phase==="complete"?this.activeFullSyncPlaceId:null;if(a==null){r.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,r);break;case"chunk":await this.initHandler.handleInitChunk(a,i,r);break;case"complete":await this.initHandler.handleInitComplete(a,i,r);break}}catch(n){this.sendError(r,n,"handleSyncInit")}}async handleSyncUpdate(e,r){try{Zz(e.body);let n=Uz(e.body);if(!n.success){r.status(400).json({error:"Validation error",message:n.error});return}let i=n.data,a=i.placeId??this.getDefaultRuntimePlaceId();if(a==null){r.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"){r.status(409).json({error:"Conflict",message:`Full sync in progress for place ${a}`});return}o.activeClientId=i.clientId,this.touchRuntimePlace(a);let s=Oz(o,i.changes);v.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 fd(o,f)}catch(m){c.push({path:f.instancePath,error:m instanceof Error?m.message:String(m)})}o.changesSinceLastSave+=u,o.changesSinceLastSave>=Az&&(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),r.status(200).json(d)}catch(n){this.sendError(r,n,"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,r]of this.places.entries())if(r.state==="syncing"||r.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,r="runtime"){let n=e.query.placeId;if(n){let i=parseInt(n,10);if(!isNaN(i))return i}return r==="full"?this.activeFullSyncPlaceId:this.getDefaultRuntimePlaceId()}async handleSyncStatus(e,r){try{let n=this.resolveQueryPlaceId(e);if(n==null){let s={state:"idle",instanceCount:0,scriptCount:0,lastFullSync:null,lastIncrementalSync:null,syncRoot:this.config.getSyncRoot(),activeClientId:null,reverseSyncAvailable:!1,modifiedFileCount:0};r.status(200).json(s);return}let i=this.places.get(n);if(!i){let s={state:"idle",instanceCount:0,scriptCount:0,lastFullSync:null,lastIncrementalSync:null,syncRoot:this.config.getPlaceRoot(n),activeClientId:null,reverseSyncAvailable:!1,modifiedFileCount:0};r.status(200).json(s);return}let a=i.fileWatcher?.getPendingCount()??0;this.touchRuntimePlace(n);let o={state:i.state,instanceCount:i.instanceCount,scriptCount:i.scriptCount,lastFullSync:i.lastFullSync,lastIncrementalSync:i.lastIncrementalSync,syncRoot:this.config.getPlaceRoot(n),activeClientId:i.activeClientId,reverseSyncAvailable:a>0,modifiedFileCount:a,directions:i.directions,fileWatcherActive:i.fileWatcher!==null,forwardOnlyClasses:[...Fa]};r.status(200).json(o)}catch(n){this.sendError(r,n,"handleSyncStatus")}}async handleSyncStop(e,r){try{let n=e.body,i=n.placeId??this.getDefaultRuntimePlaceId();if(i==null){r.status(200).json({status:"idle",state:"idle",placeId:null,message:"No active sync place"});return}let a=this.places.get(i);if(!a){r.status(200).json({status:"idle",state:"idle",placeId:i,message:`No sync context for place ${i}`});return}if(n.clientId&&a.activeClientId&&n.clientId!==a.activeClientId&&(a.activeFullSyncSessionId!==null||a.state==="syncing"||a.state==="initializing")){r.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=y_.join(s,`explorer_tmp_${o}`);await Ga.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),v.info("Sync stopped",{placeId:i,reason:n.reason??"requested"}),r.status(200).json({status:"stopped",state:"idle",placeId:i})}catch(n){this.sendError(r,n,"handleSyncStop")}}async handleSyncConfig(e,r){try{if(e.method==="GET"){r.status(200).json(this.config.getConfig());return}let n=Fz(e.body);if(!n.success){r.status(400).json({error:"Validation error",message:n.error});return}let i=n.data;i.propertyMode!==void 0&&this.config.setPropertyMode(i.propertyMode),i.maxDepth!==void 0&&this.config.updateConfig({maxDepth:i.maxDepth}),i.maxInstances!==void 0&&this.config.updateConfig({maxInstances:i.maxInstances}),r.status(200).json({status:"updated",config:this.config.getConfig()})}catch(n){this.sendError(r,n,"handleSyncConfig")}}async initialize(){try{await this.config.loadFromMeta(),await this.initHandler.cleanupStaleTempDirs(),v.info("SyncController initialized")}catch(e){v.error("SyncController initialization failed",e instanceof Error?e:new Error(String(e)))}}async shutdown(){try{this.places.clear(),v.info("SyncController shut down")}catch(e){v.error("SyncController shutdown error",e instanceof Error?e:new Error(String(e)))}}async atomicWriteFile(e,r){let n=e+".tmp."+EJ().slice(0,8);try{await Ga.writeFile(n,r,"utf-8"),await Ga.rename(n,e)}catch(i){throw await Ga.unlink(n).catch(()=>{}),i}}async handlePreCheck(e,r){try{await this.apiHandler.handlePreCheck(e,r)}catch(n){this.sendError(r,n,"handlePreCheck")}}async handleSyncDirections(e,r){try{await this.apiHandler.handleSyncDirections(e,r)}catch(n){this.sendError(r,n,"handleSyncDirections")}}async handleForwardRestoreList(e,r){try{await this.apiHandler.handleForwardRestoreList(e,r)}catch(n){this.sendError(r,n,"handleForwardRestoreList")}}async handleReversePending(e,r){try{await this.reverseHandler.handleReversePending(e,r)}catch(n){this.sendError(r,n,"handleReversePending")}}async handleReverseSyncChanges(e,r){try{await this.reverseHandler.handleReverseSyncChanges(e,r)}catch(n){this.sendError(r,n,"handleReverseSyncChanges")}}async handleReverseSyncResult(e,r){try{await this.reverseHandler.handleReverseSyncResult(e,r)}catch(n){this.sendError(r,n,"handleReverseSyncResult")}}async handleResolveConflict(e,r){try{await this.reverseHandler.handleResolveConflict(e,r)}catch(n){this.sendError(r,n,"handleResolveConflict")}}async handleReverseRescan(e,r){try{await this.reverseHandler.handleReverseRescan(e,r)}catch(n){this.sendError(r,n,"handleReverseRescan")}}async handleSyncHistory(e,r){try{await this.apiHandler.handleSyncHistory(e,r)}catch(n){this.sendError(r,n,"handleSyncHistory")}}async startFileWatcherForPlace(e){if(e.state!=="syncing"){v.debug("Skipping file watcher start - place not syncing",{placeId:e.placeId,state:e.state});return}e.fileWatcher&&await e.fileWatcher.stop();let r=y_.join(this.config.getPlaceRoot(e.placeId),"explorer");e.fileWatcher=new dd(r,e.index),e.writer.setOnWriteCallback(n=>{e.fileWatcher?.suppressPath(n)}),e.fileWatcher.setDirectionChecker(n=>{let i=az(n);return e.directions[i]}),e.fileWatcher.setOnForwardViolation(n=>{e.forwardRestoreQueue.includes(n)||(e.forwardRestoreQueue.push(n),v.info("Forward violation queued for restore",{placeId:e.placeId,relativePath:n,queueSize:e.forwardRestoreQueue.length}))}),await e.fileWatcher.start(),v.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,r){return this.apiHandler.getHistoryDirect(e,r)}getDirectionsDirect(e){return this.apiHandler.getDirectionsDirect(e)}getProgressDirect(e){return this.apiHandler.getProgressDirect(e)}async readSyncedFile(e,r){return this.apiHandler.readSyncedFile(e,r)}async writeSyncedFile(e,r,n){await this.apiHandler.writeSyncedFile(e,r,n)}async executeViaDisk(e,r){return this.apiHandler.executeViaDisk(e,r)}sendError(e,r,n){let i=r instanceof Error?r.message:String(r);if(i.includes("Path traversal detected")){e.status(403).json({error:"Forbidden",message:i});return}let a=r.code;if(a==="ENOSPC"||a==="EPERM"||a==="EACCES"){e.status(500).json({error:"Disk error",message:i});return}v.error(`SyncController.${n} failed`,r instanceof Error?r:new Error(i)),e.status(500).json({error:"Internal error",message:`${n}: ${i}`})}};import{randomUUID as TJ}from"crypto";function Hz(t,e){let r=TJ(),n=r.replace(/-/g,"").substring(0,8).toUpperCase(),i=`${n.substring(0,4)}-${n.substring(4,8)}`;return{config:t,app:e,instanceId:r,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,clientModeLastHealthSuccessAt:null,clientModeLastHealthFailureAt:null,clientModeLastHealthError:null,historyManager:null,analyticsManager:null,licenseState:null,syncController:null,internalActionExecutor:null,playtestControlCommand:null,aiClientName:"",pluginVersion:"",syncedSessionToken:null}}var Bz=ro(Jp(),1);function Vz(t){let e=Bz.default.json({limit:"5mb"});t.app.use((r,n,i)=>{if(r.path.startsWith("/sync/")){i();return}e(r,n,i)}),t.app.use((r,n,i)=>{n.setHeader("Access-Control-Allow-Origin","http://localhost:3002"),n.setHeader("Access-Control-Allow-Methods","GET, POST, OPTIONS"),n.setHeader("Access-Control-Allow-Headers","Content-Type"),i()}),t.app.use((r,n,i)=>{v.debug(`${r.method} ${r.path}`,{ip:r.ip}),i()})}import{randomUUID as Ec}from"crypto";function CJ(){let t=process.env.WEPPY_ROBLOX_MCP_VERSION?.trim();return t||null}var Je=CJ()??"1.5.4";function Wz(t){let e=new Set;t.aiClientName&&e.add(t.aiClientName);for(let r of t.mcpInstances.values())r.aiClientName&&e.add(r.aiClientName);return Array.from(e)}function RJ(t){let r=Date.now();return Array.from(t.pluginClients.values()).filter(n=>r-n.lastSeen<1e4)}function Jz(t){let r=Date.now();for(let[n,i]of t.mcpInstances)i.lastSeen&&r-i.lastSeen>15e3&&(t.mcpInstances.delete(n),v.debug("Removed stale MCP instance",{instanceId:n,lastSeen:i.lastSeen}))}function Gz(t,e,r){let n=e.query.clientId;if(n&&t.pluginClients.has(n)){let i=t.pluginClients.get(n);i.lastSeen=Date.now()}try{let i=e.body;if(!i||!Array.isArray(i.selection)||typeof i.count!="number"){v.warn("Invalid selection update request",{body:i}),r.status(400).json({error:"Invalid request body"});return}let a=n||"unknown",o=Date.now();t.cachedSelectionMap.set(a,{selection:i.selection,count:i.count,timestamp:o,clientId:a}),v.debug("Selection cache updated",{count:i.count,clientId:a,timestamp:o}),r.json({status:"ok",timestamp:o})}catch(i){v.error("Error handling selection update",i),r.status(500).json({error:"Internal server error"})}}function Kz(t,e,r){let n=parseInt(e.query.maxAge)||3e4,i=Pc(t,n);i?r.json({cached:!0,...i}):r.json({cached:!1,message:"No cached selection available"})}function Pc(t,e=3e4,r){if(t.cachedSelectionMap.size===0)return null;let n;if(r)n=t.cachedSelectionMap.get(r);else for(let a of t.cachedSelectionMap.values())(!n||a.timestamp>n.timestamp)&&(n=a);if(!n)return null;let i=Date.now()-n.timestamp;return e===0||i<=e?n:null}function Xz(t,e,r){try{let n=e.body;if(!n.clientId){r.status(400).json({error:"Missing clientId"});return}let i=t.pluginClients.get(n.clientId),a=Date.now(),o={clientId:n.clientId,projectName:n.projectName,placeName:n.placeName,placeId:n.placeId,connectedAt:i?.connectedAt||a,lastSeen:a,commandsProcessed:i?.commandsProcessed||0,connectionType:"polling"};t.pluginClients.set(n.clientId,o),t.pendingCommands.has(n.clientId)||t.pendingCommands.set(n.clientId,[]),n.pluginVersion&&(t.pluginVersion=n.pluginVersion),t.analyticsManager&&(n.pluginVersion&&t.analyticsManager.setPluginVersion(n.pluginVersion),t.analyticsManager.trackPluginConnected()),v.info("Plugin client registered",{clientId:n.clientId,projectName:n.projectName,placeName:n.placeName,isReconnect:!!i}),r.json({status:"ok",clientId:n.clientId,serverInstanceId:t.instanceId,sessionId:t.sessionId,mcpVersion:Je,connectedAt:o.connectedAt,aiClientNames:Wz(t),serverStartTime:t.startTime,mcpInstanceCount:t.mcpInstances.size+1})}catch(n){v.error("Error registering plugin client",n),r.status(500).json({error:"Internal server error"})}}function Yz(t,e,r){let n=e.body?.clientId;if(!n){r.status(400).json({error:"Missing clientId"});return}let i=t.pluginClients.has(n);t.pluginClients.delete(n),t.pendingCommands.delete(n),v.info("Plugin client unregistered",{clientId:n,existed:i}),r.json({status:"ok",existed:i})}function Qz(t,e,r){try{let n=e.body;if(!n.instanceId){r.status(400).json({error:"Missing instanceId"});return}let i=Date.now(),a={instanceId:n.instanceId,pid:n.pid,connectedAt:i,isServer:!1,lastSeen:i};n.aiClientName&&(a.aiClientName=n.aiClientName),n.cwd&&(a.cwd=n.cwd),t.mcpInstances.set(n.instanceId,a),v.info("MCP instance registered (client mode)",{instanceId:n.instanceId,pid:n.pid,cwd:n.cwd}),r.json({status:"ok",instanceId:n.instanceId,serverInstanceId:t.instanceId,sessionId:t.sessionId,mcpVersion:Je,mcpInstanceCount:t.mcpInstances.size+1})}catch(n){v.error("Error registering MCP instance",n),r.status(500).json({error:"Internal server error"})}}function ej(t,e,r){let n=e.body?.instanceId;if(!n){r.status(400).json({error:"Missing instanceId"});return}let i=t.mcpInstances.has(n);t.mcpInstances.delete(n),v.info("MCP instance unregistered",{instanceId:n,existed:i}),r.json({status:"ok",existed:i})}function tj(t,e,r){let{instanceId:n,aiClientName:i}=e.body;if(n&&i){let a=t.mcpInstances.get(n);a&&(a.aiClientName=i)}r.json({status:"ok"})}function rj(t){let r=Date.now();for(let[n,i]of t.pluginClients)r-i.lastSeen>3e4&&(t.pluginClients.delete(n),t.pendingCommands.delete(n));return Jz(t),{serverInstanceId:t.instanceId,sessionId:t.sessionId,mcpVersion:Je,uptime:r-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()},...Array.from(t.mcpInstances.values())],mcpInstanceCount:t.mcpInstances.size+1}}function nj(t,e){e.json(rj(t))}function ij(t,e,r){let n=e.query.instanceId;n&&t.mcpInstances.has(n)&&(t.mcpInstances.get(n).lastSeen=Date.now()),Jz(t);let i=RJ(t),a=t.sseClients.size,c={...{status:"online",connectedClients:a+i.length,queuedCommands:t.commandQueue.size,uptime:Date.now()-t.startTime,version:Je,isClientMode:t.isClientMode,pid:process.pid,sessionId:t.sessionId},instanceId:t.instanceId,mcpInstanceCount:t.mcpInstances.size+1,aiClientNames:Wz(t),pluginVersion:t.pluginVersion||void 0,sseClients:a,pollingClients:i.length,pluginClients:i.map(l=>({clientId:l.clientId,projectName:l.projectName,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}}:{}};r.json(c)}function aj(t,e,r,n){let i=e.ip||e.socket.remoteAddress||"";if(!(i==="127.0.0.1"||i==="::1"||i==="::ffff:127.0.0.1"||i==="localhost")){v.warn("Shutdown request rejected from non-localhost",{ip:i}),r.status(403).json({error:"Forbidden: localhost only"});return}v.info("Shutdown request received, initiating graceful shutdown",{requestedBy:i,uptime:Date.now()-t.startTime}),r.json({status:"shutting_down",message:"Server will shutdown gracefully",pid:process.pid}),setTimeout(async()=>{try{await n(),v.info("Graceful shutdown completed"),process.exit(0)}catch(o){v.error("Error during graceful shutdown",o),process.exit(1)}},100)}async function wd(t){if(t.isClientMode)try{let e=await fetch(`${t.baseUrl}/connection-info`);if(e.ok)return await e.json()}catch(e){v.warn("Failed to fetch connection info from server",{error:e})}return rj(t)}var zJ={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 Sd(t){return zJ[t]||"plugin"}var kd=100;function oj(t,e,r){v.info("Plugin connected via SSE"),r.setHeader("Content-Type","text/event-stream"),r.setHeader("Cache-Control","no-cache"),r.setHeader("Connection","keep-alive"),t.sseClients.add(r),x_(r,{event:"command",id:Ec(),data:{action:"connected",requestId:Ec(),params:{serverVersion:Je,timestamp:Date.now()}}});let n=setInterval(()=>{x_(r,{event:"command",id:Ec(),data:{action:"keepalive",requestId:Ec(),params:{timestamp:Date.now()}}})},3e4);r.on("close",()=>{v.info("Plugin disconnected from SSE"),clearInterval(n),t.sseClients.delete(r)})}function x_(t,e){let r=JSON.stringify(e.data);t.write(`event: ${e.event}
118
118
  `),t.write(`id: ${e.id}
119
119
  `),t.write(`data: ${r}
120
120