@spawnite/dev-server 0.1.0-beta.15

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/LICENSE.md ADDED
@@ -0,0 +1,169 @@
1
+ # Spawnite License 1.0
2
+
3
+ <https://spawnite.com/license/1.0>
4
+
5
+ Copyright (c) 2026 Daniel Zarinski and David Zarinski.
6
+
7
+ ## Summary
8
+
9
+ This summary is not part of the terms and does not replace them.
10
+
11
+ - Using the Spawnite engine, tools and development room is free. Building and testing your games and their tools is always allowed, on your machines and through the services and agents that work for you, including local multiplayer.
12
+ - Games that include the software are made public only through Spawnite. To publish one anywhere else, ask us at dev@spawnite.com. We decide each request case by case, and a yes is a written license that says what it covers.
13
+ - Your game's own code, assets and earnings share are yours, including what you write yourself inside a copied plugin. These terms do not reach them, and you may take them to any engine.
14
+ - A plugin the engine ships as readable source, in a package's `readable` folder, is yours to read, copy into your game and change. A copy stays under these terms.
15
+ - Everything else ships closed: use it in your games, read it, format it, step through it and patch your installed copy to build and fix them, but don't rebuild it in source or use it to build, train or test anything that competes.
16
+ - You may not redistribute the software on its own or inside another engine or SDK, run it or the development room as a hosting service, or build a competing engine or platform.
17
+ - If Spawnite stops hosting games for 90 days, you may publish and run your own games elsewhere on the versions you have.
18
+ - No royalty. Templates and kits carry a license per file: most are MIT-0, and a copied plugin stays under these terms.
19
+
20
+ ## Acceptance
21
+
22
+ These terms offer their licenses only to a person who accepts them, except Player Permission and the permission to download a copy to read these terms. You accept by choosing an acceptance control that shows these terms, such as one Spawnite's tools or website present, by sending the licensor written acceptance that names this version, or by developing, testing or publishing a game with the software, which these terms come with. If you accept for an entity, you confirm you may bind it. Downloading or installing a package, receiving it as a dependency, or visiting a page does not accept these terms by itself, and gives no license but Player Permission.
23
+
24
+ In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses, including the Limitations, whether or not an activity they name would otherwise need copyright or patent permission.
25
+
26
+ ## Copyright License
27
+
28
+ The licensor grants you a copyright license for the software to do everything you might do with the software that would otherwise infringe the licensor's copyright in it for any permitted purpose. However, you may only distribute the software according to Distribution License, make changes and new works based on the software according to Changes and New Works License, and copy and change source-available parts according to Source-Available Parts.
29
+
30
+ ## Permitted Purposes
31
+
32
+ These are the permitted purposes:
33
+
34
+ - development operations for your games and their tools, including private testing with the development room server, but not by making a game available to the public;
35
+ - making a game available to the public through Spawnite, as Games describes; and
36
+ - studying the software and publishing explanations, reviews, criticism and security findings about it, without publishing the closed parts, what anyone recovers of their source, or more of a source-available part than short quotations. This purpose grants no permission to develop a competing product.
37
+
38
+ ## Games
39
+
40
+ You may supply the software as a functional part of a game, bundled with the game or loaded from a shared engine release Spawnite provides, but not as a reusable engine, framework or SDK for others. You may make a game that includes or requires any part of the software available to the public only through Spawnite, which includes its delivery to and running in players' browsers and apps. Making it available to the public any other way, including on your own website or on another store or platform, is not a permitted purpose.
41
+
42
+ To publish a game outside Spawnite, ask the licensor at dev@spawnite.com. The licensor decides each request on its own case and need not grant any. A request, an acknowledgement or a discussion grants nothing. A permission takes effect only as a written license from the licensor that names the games, the versions of the software, where they may be published, whether and where a room server may run for them, how long it lasts and any charge, and it overrides these terms only as far as it says.
43
+
44
+ Publishing your game's source code, such as in a public repository, is not making the game available to the public, as long as it holds no closed part of the software and every copy of a source-available part in it keeps its notice and stays under these terms.
45
+
46
+ ## Player Permission
47
+
48
+ Anyone may receive, cache and run the copies of the software delivered to play a game made public as Games, a written license from the licensor or If Spawnite Stops Hosting Games allows. This covers what a browser or an app does to play the game, and gives no permission to extract those copies or use them for anything else.
49
+
50
+ ## Your Game
51
+
52
+ You keep the ownership of your game's original code, assets and other content, and of what you write yourself in a copy of a source-available part, without copying the software. These terms give the licensor no rights in them and assign it nothing, and do not limit what you do with them apart from the software. You may use them with any other engine and on any other platform. In a copy of a source-available part in your game, what comes from the software, changed or not, stays part of the software. Code does not become part of the software because it calls the software, sits in the same file as it, or ships beside it. Your game's saves, world data, screenshots, recordings and other outputs are yours.
53
+
54
+ Templates, kits, starter code and examples carry the license their files name. A file that names the MIT No Attribution License (MIT-0) is under MIT-0, not these terms. A file that names these terms is a source-available part. Third-party material in them stays under its own license. Moving, renaming or changing a file's notice does not change its license. What a game earns on Spawnite is governed by Spawnite's creator terms, not this license, and this license charges no royalty or fee.
55
+
56
+ ## Distribution License
57
+
58
+ The licensor grants you an additional copyright license to distribute copies of the software, with changes permitted by these terms, but only as part of a game according to Games, and to your company and private testers for a permitted purpose. Giving the software to a service that processes it only for you and for your permitted purposes, such as a private code repository, a continuous-integration service or an AI coding service, is not distributing it.
59
+
60
+ ## Changes and New Works License
61
+
62
+ The licensor grants you an additional copyright license to make changes and new works based on the software for any permitted purpose, as follows. You may change your installed copy of a closed part, such as by a patch, to build, test and fix your games, but you may not write a closed part's implementation into your own source code, or distribute a changed closed part except as part of a game according to Games. The software in a change or new work stays under these terms, and distributing the change or new work distributes that software. No grant in these terms permits developing a competing product.
63
+
64
+ ## Source-Available Parts
65
+
66
+ The licensor grants you an additional copyright license to read the source-available parts, to copy them into a game or a tool for your games, and to change those copies, for any permitted purpose. You may distribute a copy, changed or not, only as part of a game according to Games, to your company and private testers, and as Games allows for your game's source. You may give a copy, changed or not, to people you choose who have accepted these terms, for a game or a contribution you are working on together, privately or through a channel Spawnite provides for sharing plugins and kits. You may not make a copy available to the public any other way, such as in a public collection of engine code, a package registry or a website. Each recipient gets its license from the licensor, not from you.
67
+
68
+ ## Contributions
69
+
70
+ A contribution is a change to the software you expressly offer the licensor for inclusion in it, such as by a pull request or a message that says so. Uploading a game to publish or test it, sending a bug report or a bug bundle, and letting the licensor run your game are not contributions. You keep the ownership of your contribution, and grant the licensor a non-exclusive, perpetual, irrevocable, worldwide, royalty-free license, under your copyright and any patent claims you can license that the contribution infringes, to use, change, distribute and sublicense it as part of the software. The grant covers the contribution alone, not your game's other code, assets or confidential information. Nothing in these terms requires you to offer one.
71
+
72
+ ## Limitations
73
+
74
+ Even for a permitted purpose, you may not:
75
+
76
+ - distribute the software except as Distribution License, Source-Available Parts and Games allow;
77
+ - provide others a hosted or managed service that gives reusable access to a substantial part of the software's engine, authoring, simulation or development room features; development operations, private testing, play under Games and Player Permission, and what If Spawnite Stops Hosting Games allows are not such a service;
78
+ - run the development room server, or any other part of the software, to serve the players of a game made available to the public, except as a written license from the licensor or If Spawnite Stops Hosting Games allows;
79
+ - rebuild or try to rebuild the implementation of a closed part in source form, or use such a rebuild from anyone else, except within development operations or a right Rights the Law Keeps preserves; reading, formatting, stepping through and patching the closed parts within development operations is allowed, and you may not publish or share what you recover;
80
+ - use the software, its expression, its outputs or its observed behaviour to develop, train, specify, validate or provide a competing product, including a machine-learning model that is or powers one, such as by generating specifications, training data or comparison tests for it; or
81
+ - direct, authorise or knowingly help another person or an automated agent to do what these terms forbid you.
82
+
83
+ These limits do not forbid development operations, ordinary reviews, criticism and benchmarks, porting or rebuilding your own games on another engine, or plugins, kits and tools that work only with the software. They claim nothing in general knowledge, ideas or independently written software.
84
+
85
+ ## Competition
86
+
87
+ A competing product is a game engine product that substitutes for the Spawnite engine, or a service that substitutes for Spawnite's service of publishing or hosting games for independent creators. Different interfaces, technical platforms, prices or distribution methods do not stop a product from competing, and a free product can compete. Marketing a product as a substitute for the software or for Spawnite is evidence that it competes.
88
+
89
+ A game does not compete because players choose it over another game or service, or because its players make, share or play levels and other content inside it. A plugin, kit or tool that works only with the software does not compete because it replaces one optional plugin or development tool.
90
+
91
+ ## If Spawnite Stops Hosting Games
92
+
93
+ This section takes effect if, for 90 consecutive days, Spawnite accepts no new game or new version of a game from creators generally, or makes no game of a creator other than the licensor available for public play, or if the licensor publicly announces that Spawnite will permanently stop either. A few demonstration, test or selected games staying available does not stop the first two from taking effect.
94
+
95
+ Then, for every version of the software the licensor published under these terms before that date, including your changes to those versions:
96
+
97
+ - Games no longer limits where you make your games available;
98
+ - you, or a service provider working for you, may run the development room server to serve your games' players, and doing so is not a hosted or managed service under Limitations; and
99
+ - a service that does only this, for your own games, does not compete with Spawnite.
100
+
101
+ This release is automatic, permanent, needs no announcement or further permission, stays in effect if Spawnite later resumes hosting, and cannot be taken away by a later version of these terms. It does not permit offering an engine, or a publishing or hosting platform for independent creators, and gives no access to systems the licensor never published.
102
+
103
+ ## Changes to These Terms
104
+
105
+ The licensor may publish new versions of these terms, each with a new version number and its own URL. A new version applies to versions of the software the licensor publishes after it, and to your use of earlier versions 30 days after the licensor gives notice on spawnite.com and by email to the address on your Spawnite account, if you have one. No new version can narrow Your Game, add a royalty or fee, narrow development operations for your games under Permitted Purposes, narrow Source-Available Parts for copies already in your games, narrow a written license the licensor granted, or narrow If Spawnite Stops Hosting Games. A new version does not make anything you did before it a violation.
106
+
107
+ ## Other Software
108
+
109
+ The software includes components that others license under their own terms, listed in the THIRD-PARTY-NOTICES file that comes with it. Those terms, not these, govern those components. These terms neither replace nor restrict a permission a third-party license gives, in source, compiled or bundled form, and the licensor naming material source-available or closed does not override that license.
110
+
111
+ ## Notices
112
+
113
+ You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms or the URL for them above, as well as copies of any plain-text lines beginning with `Required Notice:` that the licensor provided with the software. When you distribute source files of a source-available part, keep their notices with them. In a compiled, minified or bundled game, you may put the notices in a license file or a legal notices page that ships with the game. Compiling, minifying or moving a notice does not change the license.
114
+
115
+ ## Patent License
116
+
117
+ The licensor grants you a patent license for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software.
118
+
119
+ ## Rights the Law Keeps
120
+
121
+ These terms do not restrict a right the law does not let the parties restrict by agreement, such as to observe, study or test how the software works, to make a backup copy, to correct errors, or to decompile it so that an independently created program can interoperate with it, but only to the extent and for the purposes that law requires. These terms do not waive a fair use or fair dealing defence to a copyright claim, and that defence alone does not excuse a breach of these terms. They claim no copyright in ideas, principles, functionality or methods of operation, and no obligation in them applies where the law forbids it.
122
+
123
+ ## No Other Rights
124
+
125
+ These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else. These terms do not imply any other licenses. No trademark permission is granted.
126
+
127
+ ## Patent Defense
128
+
129
+ If you, or an entity you act for, start a lawsuit or other proceeding claiming that the software infringes a patent, your patent license for the software ends immediately. A private report, a support message or a defence or counterclaim in a proceeding the licensor started against you does not end it.
130
+
131
+ ## Violations
132
+
133
+ The licensor may end your licenses for a material violation of these terms by written notice that names what you did and the term it breaks. Your licenses end only if you do not stop the violation and take practical steps to correct its effects within 32 days of receiving the notice; a violation you correct in time ends nothing. The licensor may end your licenses at once, by written notice, for knowingly distributing the software as an engine or knowingly developing or providing a competing product. Doing what Rights the Law Keeps preserves is not a violation.
134
+
135
+ When your licenses end, you must stop what needed them. Ending your licenses does not take your game's own content, does not affect material under other licenses, and does not end a contribution license you granted. Games you made available through Spawnite before your licenses ended may stay available, and keep earning, as Spawnite's creator terms provide.
136
+
137
+ ## No Liability
138
+
139
+ As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.
140
+
141
+ ## Definitions
142
+
143
+ The **licensor** is Daniel Zarinski and David Zarinski, and their successors.
144
+
145
+ The **software** is the software the licensor makes available under these terms: the Spawnite engine, schema, CLI, MCP server, dev server, devtools, UI kit, testing tools and development room server, with their type definitions and documentation, in every form the licensor provides them, including the engine release Spawnite serves to players' browsers and apps, and every copy and adaptation of their expression.
146
+
147
+ The **source-available parts** are the files of the software under a `readable/` folder of a package, and any other file of the software whose notice names these terms as source available, with every copy, excerpt, change, translation and compiled or bundled form of them, whether or not it keeps the notice. The **closed parts** are every other part of the software the licensor provides, including its compiled, minified and type definition files, its documentation, and the engine release Spawnite serves to players. "Closed" names the permissions these terms give, not whether a file can be read.
148
+
149
+ **Development operations** are what is reasonably needed to develop, test, profile, secure, diagnose and maintain your games and the tools you make for them: reading, formatting, stepping through, tracing, patching and running the software, headless or not, replays, compatibility tests and temporary instrumentation, by you and by the people, services and automated agents working for you, on machines you control or that run for you, such as rented cloud machines, continuous-integration services and AI coding services. You must not let those working for you reuse the software for their own purposes, develop a competing product, or train a general-purpose model on it.
150
+
151
+ **Spawnite** is the game service the licensor operates at spawnite.com, including its apps and the embeds and links it provides for games published on it.
152
+
153
+ A **game** is software that includes or requires any part of the software to be played, with the content it ships with. A level, asset, save or recording you share on its own is not a game.
154
+
155
+ To **make a game available to the public** is to let anyone play it other than you, your company and private testers. **Private testers** are people you choose one by one, who reach the game through access you control for testing. Testers may be recruited in public, but access open to everyone who signs up, asks, pays or follows a link is public. Calling a release a test, preview or beta does not make it private.
156
+
157
+ The **development room server** is the room server the licensor distributes for development and testing, and every copy and permitted adaptation of it, wherever it runs and whatever it is called.
158
+
159
+ A **game engine product** is a reusable environment for developers to make, simulate or render games they develop independently of each other. A game, and the tools made only to develop or test that game, are not game engine products because they hold simulation, rendering or editing features, and neither is a plugin, kit or tool that works only with the software.
160
+
161
+ A **product** can be a good or service, or a combination of them.
162
+
163
+ **You** refers to the individual or entity accepting these terms.
164
+
165
+ **Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. **Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
166
+
167
+ **Your licenses** are all the licenses granted to you for the software under these terms.
168
+
169
+ **Use** means accessing, running, copying, changing, analysing or otherwise employing the software, or employing its outputs or observed behaviour.
package/README.md ADDED
@@ -0,0 +1,24 @@
1
+ # @spawnite/dev-server
2
+
3
+ The routes a game's Vite dev server answers for the devtools page and for the agent's tools, as one [Hono](https://hono.dev) app under `/__`:
4
+
5
+ - `POST /__shots` saves a screenshot the page took into `<game>/.spawnite/shots/` and answers its path.
6
+ - `POST /__chat` saves a message the creator typed in the devtools chat into `<game>/.spawnite/chat/` as an MCP tool result, so the agent's `read_chat` returns it as it is.
7
+ - `GET /__map?map=<name>` answers a map's `terrain` block, and `POST /__map` runs one map editor stroke or fill, writes an undo's values back, settles a moved base, writes one entry of the map's `materials`, writes placed props into the map's `props`, a null one taken out, or sets the map's `sea` into `src/maps/<name>.json` through the one writer every map-file write goes through, `updateMapFile`. Both answer only the dev server's own page.
8
+ - `GET /__tab` lists the creator's open tabs of the game, and `POST /__tab` runs a JavaScript expression in the one they looked at last and answers its value, over Vite's hot-reload socket, for the agent's playtest tools. Both take the token the server writes to `<game>/.spawnite/servers/<port>.json` while it listens.
9
+
10
+ A game adds the plugin beside the engine's in its Vite config:
11
+
12
+ ```ts
13
+ import { devServer } from "@spawnite/dev-server/vite";
14
+
15
+ export default defineConfig({ plugins: [spawnite(), devServer()] });
16
+ ```
17
+
18
+ The plugin also writes a small client into each page it serves in development, and none into a build, which joins the tab to that socket.
19
+
20
+ The `.spawnite/` folder is git-ignored; the map files `/__map` writes are the game's own, tracked. The wiki's [dev server](https://wiki.spawnite.com/engine/testing/dev-server/) page describes each route and how a message reaches the agent.
21
+
22
+ ## License
23
+
24
+ The Spawnite License 1.0, in [LICENSE.md](LICENSE.md). [License](https://wiki.spawnite.com/platform/license/) on the wiki says in plain words what it lets you do. The code this package bundles from other projects keeps its own licenses, listed in `THIRD-PARTY-NOTICES` beside the built files.
@@ -0,0 +1 @@
1
+ import{dirname as e,join as t}from"node:path";import{readFileSync as n,rmSync as r,rmdirSync as i}from"node:fs";var a=[`.spawnite`,`servers`];function o(o,{port:s,pid:c}){let l=t(o,...a),u=t(l,`${s}.json`),d;try{d=JSON.parse(n(u,`utf8`))}catch{return}if(typeof d==`object`&&d&&`pid`in d&&d.pid===c){r(u,{force:!0});for(let t of[l,e(l)])try{i(t)}catch{break}}}var s=`spawnite-installed:`,c={resolveId(e){return e.startsWith(s)?`\0${e}`:void 0},load(e){return e.startsWith(`\0${s}`)?`export * from ${JSON.stringify(e.slice(20))};`:void 0}};function l(e,t){let n=/^[@\w]/.test(t)&&!t.includes(`:`);return e.ssrLoadModule(n?s+t:t)}export{o as i,l as n,a as r,c as t};
@@ -0,0 +1,7 @@
1
+ import{chatFolderName as e,chatPath as t,chatPost as n,chatReadFolderName as r,clipFinish as i,clipMeta as a,clipName as o,clipsFolderName as s,clipsPath as c,devServerFile as l,formatClipLength as u,formatClipSize as d,gameMaterialsName as f,mapBaseSchema as p,mapPath as m,mapPost as h,mapSchema as g,mapTerrainSchema as _,readPaintGrid as ee,readTerrainGrid as v,sceneName as te,serversFolderName as ne,shotPost as re,shotsFolderName as ie,shotsPath as ae,spawniteFolderName as y,tabPath as oe,tabPost as se,tabTokenHeader as ce,terrainRestore as le,writePaintGrid as ue,writeTerrainGrid as de,z as b}from"@spawnite/schema";import{Hono as x}from"hono";import{HTTPException as S}from"hono/http-exception";import{basename as C,dirname as w,join as T,resolve as fe}from"node:path";import{fileURLToPath as pe,pathToFileURL as me}from"node:url";import{bodyLimit as E}from"hono/body-limit";import{existsSync as D,mkdirSync as O,readFileSync as k,readdirSync as A,renameSync as j,rmSync as M,writeFileSync as N}from"node:fs";import{CallToolResultSchema as he}from"@modelcontextprotocol/sdk/types.js";import{zValidator as ge}from"@hono/zod-validator";import{findNodeAtLocation as _e,parseTree as ve}from"jsonc-parser";import{lock as ye}from"proper-lockfile";function P(e){return T(e,y,ie)}function F(t){return T(t,y,e)}function be(e,t,n){O(e,{recursive:!0});let r=(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g,`-`),i=T(e,`${r}${t}`);for(let n=1;D(i);n+=1)i=T(e,`${r}-${n}${t}`);let a=`${i}.tmp`;return N(a,n),j(a,i),i}function xe(e,t){return be(P(e),`.png`,t)}function Se(e,t){return be(F(e),`.json`,JSON.stringify(t,null,4))}function Ce(e,t){return D(t)?A(t).filter(e=>e.endsWith(`.json`)).flatMap(n=>{let r=T(t,n);try{return[{game:e,file:r,result:he.parse(JSON.parse(k(r,`utf8`)))}]}catch(e){return console.warn(`${r} is no chat message: ${String(e)}`),[]}}):[]}function we(e,{all:t=!1}={}){let n=F(e);return[...Ce(e,n),...t?Ce(e,T(n,r)):[]].sort(Te)}function Te(e,t){let[n,r]=[e,t].map(({file:e})=>C(e,`.json`));return n<r?-1:+(n>r)}function Ee(e){let t=T(w(e),r);O(t,{recursive:!0});let n=T(t,C(e));return j(e,n),n}function De(e){return e.content.flatMap(e=>e.type===`text`?[e.text]:e.type===`resource_link`?[`Screenshot: ${pe(e.uri)}`]:[]).join(`
2
+
3
+ `)}function Oe(e){return{...e,content:e.content.flatMap(e=>{if(e.type!==`resource_link`||e.mimeType!==`image/png`)return[e];try{return[e,{type:`image`,data:k(pe(e.uri)).toString(`base64`),mimeType:`image/png`}]}catch{return[e]}})}}function ke(e){try{return process.kill(e,0),!0}catch(e){return e.code===`EPERM`}}function Ae(e){let t=T(e,y,ne);return D(t)?A(t).filter(e=>e.endsWith(`.json`)).flatMap(e=>{let n=T(t,e),r;try{r=l.parse(JSON.parse(k(n,`utf8`)))}catch{return[]}return ke(r.pid)?[r]:(M(n,{force:!0}),[])}):[]}function I(e){return ge(`json`,e,(e,t)=>{if(!e.success)return t.json({error:b.prettifyError(e.error)},400)})}function je({root:e}){let t=P(e);return new x().use(E({maxSize:1024**2})).post(`/`,I(n),n=>{let{text:r,shots:i,attachments:a}=n.req.valid(`json`),o=i.filter(e=>w(fe(e))!==t);if(o.length>0)return n.json({error:`Not a shot /__shots saved: ${o.join(`, `)}`},400);let s={content:[{type:`text`,text:r},...i.map(e=>({type:`resource_link`,uri:me(e).href,name:C(e),mimeType:`image/png`}))],structuredContent:{game:C(e),sentAt:(/* @__PURE__ */ new Date()).toISOString(),attachments:a}};return n.json({file:Se(e,s)})})}var Me=`clip.mp4`,L=`clip.json`;function R(e){return T(e,y,s)}function z(e,t){if(!o.safeParse(t).success)throw Error(`"${t}" is not a clip's name: \`spawnite clip list\` names each, as 20261002-140211.`);return T(R(e),t)}var B=e=>String(e).padStart(2,`0`);function Ne(e,t=/* @__PURE__ */ new Date){let n=`${t.getFullYear()}${B(t.getMonth()+1)}${B(t.getDate())}-${B(t.getHours())}${B(t.getMinutes())}${B(t.getSeconds())}`,r=n;for(let t=2;D(T(e,r));t+=1)r=`${n}-${t}`;return r}function V(e){let t=T(e,L);if(D(t))return a.parse(JSON.parse(k(t,`utf8`)))}function H(e){let t=R(e);return D(t)?A(t).filter(e=>o.safeParse(e).success).sort().reverse().flatMap(e=>{let n=T(t,e),r=V(n);return r?[{name:e,folder:n,meta:r}]:[]}):[]}function Pe({from:e,to:t}){if(e!==void 0||t!==void 0)return{from:(e??0)*1e3,to:(t??1/0)*1e3}}function Fe(e,t,n){let r=V(z(e,t));if(!r)throw Error(`No clip ${t}: \`spawnite clip list\` names each.`);if(!n)return r;let i=({at:e})=>e>=n.from&&e<=n.to;return{...r,frames:r.frames.filter(i),events:r.events.filter(i)}}function Ie(e,t,n){let r=z(e,t.name);return(n?t.frames.map(({file:e})=>e):[t.sheet]).map(e=>k(T(r,e)))}function Le(e,t){let n=z(e,t);if(!D(n))throw Error(`No clip ${t}: \`spawnite clip list\` names each.`);let r=V(n);return M(n,{recursive:!0,force:!0}),r}var U=e=>`${(e/1e3).toFixed(1)} s`;function Re(e){return e.map(({name:e,folder:t,meta:n})=>`${e} ${u(n.durationMs)} ${d(n.bytes)} ${n.shape} ${n.events.length} events ${t}`)}function ze(e,t){let{name:n,durationMs:r,width:i,height:a,fps:o,bytes:s,replay:c}=e;return[`Clip ${n}: ${U(r)}, ${i}x${a} at ${o} fps, ${d(s)}${c?`, replay ${c.id} at ${(c.from/1e3).toFixed(1)} to ${U(c.to)}, saved as ${c.saved}`:``}.`,`Sheet: ${T(t,e.sheet)}`,`Events: ${e.events.map(({at:e,text:t})=>`${U(e)} ${t}`).join(` · `)||`none`}`,`Stills: ${e.frames.map(({file:e,at:n})=>`${T(t,e)} (${U(n)})`).join(`, `)||`none`}`,...e.warnings.map(e=>`Warning: ${e}`)]}var Be="No replay window: the page kept no replay with a room (`?replay=off`, or a game with no room), or the clip ran on the world's steps, whose time is not the replay's, so this clip cannot be shot again in the engine.",Ve=4,He=32;function Ue(e){let t=e.req.param(`name`)??``;if(!o.safeParse(t).success)throw new S(400,{message:`"${t}" is not a clip's name, as 20261002-140211.`});return t}function We(e,{keep:t},n){if(!(H(e).length+n<t))return`This game keeps ${t} clip${t===1?``:`s`} already, its limit: delete one in the Clips section, or raise clips.keep in spawnite() in vite.config.ts.`}var W=e=>[e].flat().filter(e=>e instanceof File),G=async(e,t)=>N(e,new Uint8Array(await t.arrayBuffer()));async function Ge(e,t,n){let r=T(t,e.replaysFolderName),i=e.resolveReplay(r,n.id),a={from:n.from,to:n.to};return{events:[...e.readReplayLines(i.folder,i.id,a),...e.readWorldLines(i.folder,i.id,a)].sort((e,t)=>e.at-t.at).map(({at:t,event:r})=>({...r,at:Math.round(t-n.from),text:e.describeReplayEvent(r)})),saved:`saved/${e.saveReplayClip(r,i,a,n.saved).name}`}}function Ke({root:e,clips:t,loadModule:n,warn:r}){if(!t)return new x().all(`*`,()=>{throw new S(404,{message:`This server records no clips: the game's vite.config.ts adds spawnite() from @spawnite/engine/vite`})});let a=0,o=async()=>await n?.(`@spawnite/engine/internal/dev-server`);return new x().use((e,t)=>{if(e.req.header(`sec-fetch-site`)!==`same-origin`)throw new S(403,{message:`Only the dev server's own page records a clip`});return t()}).use(E({maxSize:(t.maxSeconds*Ve+He)*1024**2})).get(`/`,n=>n.json({clips:H(e),maxSeconds:t.maxSeconds,refusal:We(e,t,a)})).post(`/`,async n=>{let r=We(e,t,a);if(r)throw new S(409,{message:r});a+=1;try{let t=await n.req.parseBody({all:!0}),r=i.safeParse(JSON.parse(typeof t.meta==`string`?t.meta:`null`));if(!r.success)throw new S(400,{message:`The clip's meta: ${r.error.message}`});let a=r.data,[s]=W(t.video),[c]=W(t.sheet),l=W(t.frames);if(!s||!c||l.length!==a.frames.length)throw new S(400,{message:`A clip carries its video, its sheet and a still for each of its meta's ${a.frames.length} frames; this one carries ${s?`a`:`no`} video, ${c?`a`:`no`} sheet and ${l.length} stills`});let u=Ne(R(e),new Date(a.startedAt)),d=T(R(e),u);O(T(d,`frames`),{recursive:!0});try{let t=l.map((e,t)=>`frames/${String(t).padStart(2,`0`)}.webp`);await Promise.all([G(T(d,Me),s),G(T(d,`sheet.webp`),c),...l.map((e,n)=>G(T(d,t[n]),e))]);let{startedAt:r,replay:i,frames:f,...p}=a,m=[...a.warnings],h=[],g;if(!i)m.push(Be);else try{let t=await o();if(!t)throw Error(`this server loads no engine`);let n=await Ge(t,e,{...i,saved:`clip-${u}`});h=n.events,g={...i,saved:n.saved}}catch(e){m.push(`The replay window was not kept: ${e.message}`)}let _={name:u,createdAt:new Date(r).toISOString(),...p,bytes:s.size,sheet:`sheet.webp`,frames:f.map(({at:e},n)=>({file:t[n],at:e})),events:h,...g&&{replay:g},warnings:m};return N(T(d,L),JSON.stringify(_,null,4)),n.json(_)}catch(e){throw M(d,{recursive:!0,force:!0}),e}}finally{--a}}).delete(`/:name`,async t=>{let n=Ue(t),i;try{i=Le(e,n)}catch(e){throw new S(404,{message:e.message})}let a=i?.replay?.saved;if(a)try{let t=await o();t?.removeReplays(T(e,t.replaysFolderName),[a])}catch(e){r(`Clip ${n}'s replay window ${a}: ${e.message}`)}return t.json({removed:n})})}function K(e){let t=te.safeParse(e);if(!t.success)throw Error(`map ${e}: ${t.error.issues[0].message}`);if(e===f)throw Error(`map ${e}: src/maps/${f}.json holds the game's materials, not a map; name the map otherwise.`);return`src/maps/${e}.json`}var qe=80,q=` `,Je=e=>e.length>1&&e.every(t=>typeof t==`object`&&!!t&&Object.keys(t).length>1&&Array.isArray(t)===Array.isArray(e[0]));function J(e,t=0,n=0,r=0){if(typeof e!=`object`||!e)return JSON.stringify(e);let i=q.repeat(t+1),a=q.repeat(t);if(Array.isArray(e)){if(e.length===0)return`[]`;let o=`[${e.map(e=>J(e,t+1)).join(`, `)}]`;return!Je(e)&&!o.includes(`
4
+ `)&&n+o.length+r<=qe?o:`[\n${e.map((n,r)=>{let a=r<e.length-1?`,`:``;return`${i}${J(n,t+1,i.length,a.length)}${a}`}).join(`
5
+ `)}\n${a}]`}let o=Object.entries(e).filter(([,e])=>e!==void 0);return o.length===0?`{}`:`{\n${o.map(([e,n],r)=>{let a=r<o.length-1?`,`:``,s=`${i}${JSON.stringify(e)}: `;return`${s}${J(n,t+1,s.length,a.length)}${a}`}).join(`
6
+ `)}\n${a}}`}function Ye(e,t,n){let r=ve(e),i=r?.children?.at(-1);if(r?.type!==`object`||!i)throw Error(`is not a JSON object with a key`);let a=_e(r,[t]);if(!a){let r=`${q}${JSON.stringify(t)}: `,a=i.offset+i.length;return`${e.slice(0,a)},\n${r}${J(n,1,r.length)}${e.slice(a)}`}let o=a.offset-(e.lastIndexOf(`
7
+ `,a.offset)+1),s=a.parent===i?0:1;return`${e.slice(0,a.offset)}${J(n,1,o,s)}${e.slice(a.offset+a.length)}`}var Y=/* @__PURE__ */ new Map;function Xe(e,t){let n=(Y.get(e)??Promise.resolve()).catch(()=>void 0).then(t);Y.set(e,n);let r=()=>{Y.get(e)===n&&Y.delete(e)};return n.then(r,r),n}var Ze=1e4,Qe=20;async function $e(e,t,n){try{return await ye(t,{realpath:!1,lockfilePath:`${e}.lock`,stale:Ze,retries:{retries:Ze/Qe,factor:1,minTimeout:Qe,maxTimeout:Qe}})}catch(e){throw e.code===`ELOCKED`?Error(`${n} stayed locked by another writer for ${Ze/1e3} s: write it again.`,{cause:e}):e}}var et=e=>D(e)?k(e,`utf8`):void 0,X=class extends Error{};async function Z({root:e,map:t,key:n,revision:r,create:i,update:a}){let o=K(t),s=T(e,o),c=T(e,y,`maps`,`${t}.json`);return Xe(s,async()=>{O(w(c),{recursive:!0});let e=await $e(c,s,o);try{return l()}finally{await e()}});function l(){for(let e=0;e<3;e++){let e=et(s),t;if(e!==void 0)try{t=JSON.parse(e)}catch(e){throw Error(`${o}: ${e.message}`,{cause:e})}else if(i)t={...i()};else throw Error(`There is no map file ${o} to write ${n} into.`);let l=typeof t.revision==`number`?t.revision:0;if(r!==void 0&&r!==l)throw new X(`${o} is at revision ${l}, not the ${r} this write was made against: read it again and redo the edit.`);let{value:u,result:d}=a(t),f=l+1,p;try{p=e===void 0?`${J({...t,[n]:u,revision:f})}\n`:Ye(Ye(e,n,u),`revision`,f)}catch(e){throw Error(`${o}: ${e.message}`,{cause:e})}let m=g.safeParse(JSON.parse(p));if(!m.success)throw Error(`${o}: ${b.prettifyError(m.error)}`);if(et(s)!==e)continue;let h=`${c}.${process.pid}.tmp`;return O(w(s),{recursive:!0}),N(h,p),j(h,s),{file:o,text:p,revision:f,result:d}}throw Error(`${o} kept changing while ${n} was written: write it again.`)}}var tt=class extends Error{};function nt(e,t){let n=K(t),r=T(e,n);if(!D(r))return{terrain:null,revision:0};let i=g.safeParse(JSON.parse(k(r,`utf8`)));if(!i.success)throw Error(`${n}: ${b.prettifyError(i.error)}`);return{terrain:i.data.terrain??null,revision:i.data.revision??0}}async function Q(e,t,n,r,i){let a=await Z({root:e,map:t,key:`terrain`,revision:r,create:()=>n.readMap(t),update:e=>{let r=e.terrain===void 0?void 0:_.safeParse(e.terrain);if(r?.success===!1)throw Error(`${K(t)}: terrain: ${b.prettifyError(r.error)}`);let{terrain:a,result:o}=i(e,r?.data),{erased:s,...c}=a,l=s&&n.pruneErasedScatter(n.createGroundSurface({...e,terrain:a})),u=l&&Object.keys(l).length>0?{...c,erased:l}:c;return{value:u,result:{result:o,terrain:u}}}}),{result:o,terrain:s}=a.result;return{...a,result:o,terrain:s}}function $(e,t,n){let r=Math.round(e.size/e.cell)+1,i=de(t,r);return{size:e.size,cell:e.cell,base:p.parse(e),...i&&{height:i},...ue(n)}}var rt=(e,t)=>ee(t??{size:e.size,cell:e.cell});async function it({root:e,map:t,engine:n,revision:r,stroke:i,fill:a,restore:o}){let s=await Q(e,t,n,r,(e,r)=>{let s=n.createGroundSurface(e),{map:c,baseHeights:l,offsets:u}=s;if(r?.height&&JSON.stringify(p.parse(s.map))!==JSON.stringify(r.base)){let i=n.createGroundSurface({...e,...r.base,terrain:void 0}).baseHeights,a=u.filter((e,t)=>!Number.isNaN(e)&&Math.abs(i[t]-l[t])>=.001).length;if(a>0)throw new tt(`${K(t)}: the map's base moved under ${a} edited points since the terrain's last write: run spawnite map terrain keep-heights first so they keep their heights, or move-with-ground so they ride on the new base.`)}let d={size:c.size,cell:c.cell,base:l,offsets:u},f=rt(c,r),m;if(i?.tool===`paint`){let{names:e,ruled:t}=n.resolveGroundMaterials(s,f);m=n.applyPaintStroke(f,i,n=>e[t[n]])}else if(i?.tool===`scatter`&&i.erase)m=n.eraseScatter(f,i,n.placeScatter(s));else if(i?.tool===`scatter`)m=n.applyPaintStroke(f,i,()=>``);else if(i)m=n.applyTerrainStroke(d,i);else if(a){let e=a.region===void 0?-1:Object.keys(c.regions).indexOf(a.region);if(a.region!==void 0&&e<0)throw Error(`fill: ${K(t)} has no region ${a.region}; it has ${Object.keys(c.regions).join(`, `)||`none`}.`);m=n.fillTerrain({paint:f,height:d},a,([t,n])=>s.getRegionWeightsAt({x:t,z:n})[e])}else if(o){let e=le.parse(o),t=f.paint.length;if(e.points.length!==e.height.length||e.points.some(e=>e>=u.length)||e.cells.length!==e.paint.length||e.cells.some(e=>e>=t))throw Error(`restore: needs one height per point, each point below ${u.length}, and one paint per cell, each cell below ${t}`);let r=n.restoreTerrain(d,e),i=n.restorePaint(f,e);m={...r,cells:i.cells,paintBefore:i.paintBefore,paintAfter:i.paintAfter}}else throw Error(`Give a stroke, a fill or a restore.`);return{terrain:$(c,u,f),result:m}});return{file:s.file,revision:s.revision,change:s.result,terrain:s.terrain,text:s.text}}async function at({root:e,map:t,engine:n,revision:r}){let i=K(t),a=await Q(e,t,n,r,(e,t)=>{if(!t)throw Error(`${i} has no terrain: nothing to keep the heights of.`);let r=n.createGroundSurface({...e,terrain:void 0});if(r.map.size!==t.size||r.map.cell!==t.cell)throw Error(`${i}'s terrain holds another grid than its map's: run spawnite map terrain resample first.`);let a=n.createGroundSurface({...r.map,...t.base}),o=v(t.height,Math.round(t.size/t.cell)+1);for(let e=0;e<o.length;e++)Number.isNaN(o[e])||(o[e]+=a.baseHeights[e]-r.baseHeights[e]);return{terrain:$(r.map,o,rt(r.map,t)),result:ct(o)}});return{file:i,revision:a.revision,edited:a.result}}async function ot({root:e,map:t,engine:n,revision:r}){let i=K(t),a=await Q(e,t,n,r,(e,t)=>{if(!t)throw Error(`${i} has no terrain: nothing to move with the ground.`);let r=n.createGroundSurface({...e,terrain:void 0}).map;if(r.size!==t.size||r.cell!==t.cell)throw Error(`${i}'s terrain holds another grid than its map's: run spawnite map terrain resample first.`);let a=v(t.height,Math.round(t.size/t.cell)+1);return{terrain:$(r,a,rt(r,t)),result:ct(a)}});return{file:i,revision:a.revision,edited:a.result}}async function st({root:e,map:t,engine:n}){let r=K(t),i=await Q(e,t,n,void 0,(e,t)=>{if(!t)throw Error(`${r} has no terrain: nothing to resample.`);if(t.height&&JSON.stringify(p.parse(e))!==JSON.stringify(t.base))throw Error(`${r}: the map's base changed too, and a resample would record the new one under the old edits: put its size and cell back, run spawnite map terrain keep-heights, then change them again and resample.`);let i=n.createGroundSurface({...e,terrain:void 0}).map,a=Math.round(t.size/t.cell)+1,o=v(t.height,a),s=(e,n,r,a)=>{let o=new Float64Array(r*r).fill(NaN),s=e=>Math.round(((e+a)*i.cell-i.size/2+t.size/2)/t.cell-a);for(let t=0;t<r;t++)for(let i=0;i<r;i++){let a=s(t),c=s(i);a>=0&&a<n&&c>=0&&c<n&&(o[t*r+i]=e[a*n+c])}return o},c=Math.round(i.size/i.cell)+1,l=s(o,a,c,0),u=ee(t),d=c-1;return{terrain:$(i,l,{size:i.size,cell:i.cell,palette:u.palette,paint:s(u.paint,a-1,d,.5),overlay:s(u.overlay,a-1,d,.5),blend:s(u.blend,a-1,d,.5),scatter:Object.fromEntries(Object.entries(u.scatter??{}).map(([e,t])=>[e,s(t,a-1,d,.5)])),erased:u.erased}),result:ct(l)}});return{file:r,revision:i.revision,edited:i.result}}var ct=e=>e.filter(e=>!Number.isNaN(e)).length;async function lt({root:e,map:t,engine:n,name:r,look:i,whole:a=!1,revision:o}){let s=await Z({root:e,map:t,key:`materials`,revision:o,create:()=>n.readMap(t),update:e=>{let t={...e.materials};return i===null?delete t[r]:t[r]=a?i:{...t[r],...i},{value:t,result:t}}});return{file:s.file,text:s.text,revision:s.revision,materials:s.result}}async function ut({root:e,map:t,engine:n,level:r,revision:i}){let a=await Z({root:e,map:t,key:`sea`,revision:i,create:()=>n.readMap(t),update:()=>({value:r,result:r})});return{file:a.file,text:a.text,revision:a.revision,sea:a.result}}function dt(e,t){let n=Object.create(null);for(let[t,r]of Object.entries(e??{}))r!==null&&(n[t]=r);let r=[];for(let[e,i]of Object.entries(t))i===null?Object.hasOwn(n,e)&&(r.push(e),delete n[e]):n[e]=i;return{merged:n,removed:r}}async function ft({root:e,map:t,engine:n,props:r,revision:i}){let a=await Z({root:e,map:t,key:`props`,revision:i,create:n&&(()=>n.readMap(t)),update:e=>{let{merged:t,removed:n}=dt(e.props,r);return{value:t,result:n}}});return{file:a.file,text:a.text,revision:a.revision,removed:a.result}}function pt({root:e,loadModule:t,noteMapWrite:n}){return t?new x().use((e,t)=>{if(e.req.header(`sec-fetch-site`)!==`same-origin`)throw new S(403,{message:`Only the dev server's own page edits a map`});return t()}).use(E({maxSize:4*1024**2})).get(`/`,t=>{let n=t.req.query(`map`)??``;return t.json(nt(e,n))}).post(`/`,I(h),async r=>{let{map:i,revision:a,stroke:o,fill:s,restore:c,settle:l,material:u,restoreMaterial:d,props:f,sea:p}=r.req.valid(`json`),m=await t(`@spawnite/engine/internal/dev-server`),h=(t,r)=>n?.(T(e,t).replaceAll(`\\`,`/`),r);try{if(f){let{text:t,...n}=await ft({root:e,map:i,engine:m,props:f,revision:a});return h(n.file,t),r.json(n)}if(p!==void 0){let{text:t,...n}=await ut({root:e,map:i,engine:m,level:p,revision:a});return h(n.file,t),r.json(n)}if(u||d){let{text:t,...n}=await lt({root:e,map:i,engine:m,revision:a,...u??{...d,whole:!0}});return h(n.file,t),r.json(n)}if(l){let{file:t,revision:n}=await(l===`keep-heights`?at:ot)({root:e,map:i,engine:m,revision:a});return h(t,k(`${e}/${t}`,`utf8`)),r.json({file:t,revision:n,terrain:nt(e,i).terrain})}let{text:t,...n}=await it({root:e,map:i,engine:m,revision:a,stroke:o,fill:s,restore:c});return h(n.file,t),r.json(n)}catch(e){if(e instanceof X)return r.json({error:e.message,conflict:`revision`},409);if(e instanceof tt)return r.json({error:e.message,conflict:`base`},409);throw e}}):new x().all(`*`,()=>{throw new S(404,{message:`This server loads no engine to run a brush with`})})}function mt({root:e}){return new x().use(E({maxSize:16*1024**2})).post(`/`,I(re),t=>{let{png:n}=t.req.valid(`json`),r=Buffer.from(n.slice(n.indexOf(`,`)+1),`base64`);return t.json({file:xe(e,r)})})}function ht({tabs:e,token:t}){return!e||t===void 0?new x().all(`*`,()=>{throw new S(404,{message:`This server runs no Vite socket to reach a tab on`})}):new x().use((e,n)=>{if(e.req.header(ce)!==t)throw new S(403,{message:`Send the token in .spawnite/servers as ${ce}`});return n()}).get(`/`,t=>t.json({tabs:e.list()})).post(`/`,I(se),async t=>{let{expression:n,timeoutMilliseconds:r,tab:i}=t.req.valid(`json`);try{return t.json(await e.run(n,r,i))}catch(e){return t.json({error:e instanceof Error?e.message:String(e)},422)}})}function gt(e){return new x().route(ae,mt(e)).route(t,je(e)).route(oe,ht(e)).route(m,pt(e)).route(c,Ke(e)).notFound(e=>e.json({error:`No dev-server route ${e.req.method} ${e.req.path}`},404)).onError((t,n)=>t instanceof S?n.json({error:t.message},t.status):(e.warn(`${n.req.method} ${n.req.path}: ${t.message}`),n.json({error:t.message},500)))}export{we as A,Ie as C,R as D,z as E,P as F,Se as I,xe as L,Ee as M,De as N,Te as O,F as P,Fe as S,Le as T,Me as _,dt as a,H as b,st as c,ut as d,X as f,L as g,Z as h,at as i,Ae as j,Oe as k,lt as l,K as m,tt as n,ot as o,J as p,it as r,nt as s,gt as t,ft as u,ze as v,Pe as w,Ne as x,Re as y};
@@ -0,0 +1,3 @@
1
+ Third-party notices
2
+
3
+ These files include no code from other packages.
@@ -0,0 +1,372 @@
1
+ import { ClipMeta, DevServerFile, HeightGrid, ListedClip, MapTerrain, MaterialLook, Materials, PaintGrid, TabInfo, TabReply, TerrainChange, TerrainFill, TerrainMap, TerrainRestore, TerrainRestoreInput, TerrainStroke } from "@spawnite/schema";
2
+ import { Hono } from "hono";
3
+ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
4
+ interface TabBridge {
5
+ /** Every tab open on the server, the headless playtest's among them. */
6
+ list: () => TabInfo[];
7
+ /** Runs a JavaScript expression in the tab named by its id, or with
8
+ * none in the tab the creator looked at last, and resolves with its
9
+ * value, awaited. */
10
+ run: (expression: string, timeoutMilliseconds: number, tabId?: string) => Promise<TabReply>;
11
+ }
12
+ /** What the engine's plugin sets for recorded video clips. */
13
+ export interface ClipSettings {
14
+ /** Seconds after which a recording stops by itself. */
15
+ maxSeconds: number;
16
+ /** Clips kept, unfinished ones counted: past it, a start is refused. */
17
+ keep: number;
18
+ }
19
+ /** What every route module is built with. */
20
+ export interface DevServerContext {
21
+ /** The game's folder, whose `.spawnite/` the routes write. */
22
+ root: string;
23
+ /** Where a route says what went wrong: Vite's logger, or the console. */
24
+ warn: (message: string) => void;
25
+ /** The creator's open tabs, on a server Vite's socket runs on. */
26
+ tabs?: TabBridge;
27
+ /** What a caller of `/__tab` must send, as `.spawnite/servers` keeps it. */
28
+ token?: string;
29
+ /** Loads a module through the game's own Vite, as `ssrLoadModule`
30
+ * does: the `/__map` route runs the game's engine's brush. */
31
+ loadModule?: (specifier: string) => Promise<unknown>;
32
+ /** Called with a map file's path and the text the route wrote to it,
33
+ * so the server does not reload the page that made the write. */
34
+ noteMapWrite?: (path: string, text: string) => void;
35
+ /** The clip settings of the game's `spawnite()`; without them the
36
+ * server records no clips. */
37
+ clips?: ClipSettings;
38
+ }
39
+ /** Every dev-server route, one module each; a new route is one more
40
+ * `.route()` line. An error answers `{ error }` as JSON. */
41
+ export declare function createDevServerApp(context: DevServerContext): Hono;
42
+ /** A message as read from a game's chat folder. */
43
+ export interface StoredMessage {
44
+ /** The game's folder. */
45
+ game: string;
46
+ file: string;
47
+ result: CallToolResult;
48
+ }
49
+ export declare function resolveShotsFolder(root: string): string;
50
+ export declare function resolveChatFolder(root: string): string;
51
+ /** Saves a PNG under the game's shots folder and returns its path. */
52
+ export declare function writeShot(root: string, png: Uint8Array): string;
53
+ /** Saves an unread chat message and returns its path. */
54
+ export declare function writeMessage(root: string, message: CallToolResult): string;
55
+ /** The game's unread messages, or with all the read ones too, oldest
56
+ * first. */
57
+ export declare function listMessages(root: string, { all }?: {
58
+ all?: boolean;
59
+ }): StoredMessage[];
60
+ /** Oldest first, by the stamp that names each file: a second file in one
61
+ * millisecond, whose name carries a count, sorts after the first. */
62
+ export declare function compareMessages(first: StoredMessage, second: StoredMessage): number;
63
+ /** Moves an unread message into the read folder beside it and returns its
64
+ * new path. */
65
+ export declare function markRead(file: string): string;
66
+ /** The message as markdown: each text as it is, each screenshot as its
67
+ * path. The hook, the CLI and the MCP's channel print it alike. */
68
+ export declare function renderMessage(result: CallToolResult): string;
69
+ /** The message with each linked PNG read into an image block after its
70
+ * link, so an agent sees it without a second call. A shot deleted since
71
+ * keeps its link alone. */
72
+ export declare function inlineShots(result: CallToolResult): CallToolResult;
73
+ /** The dev servers running for the game, as each wrote itself to
74
+ * `.spawnite/servers` while it listens. A file whose process is gone, as
75
+ * after a kill, is removed; a file that is no server's is skipped. */
76
+ export declare function listServers(root: string): DevServerFile[];
77
+ export declare const clipVideoFile = "clip.mp4";
78
+ export declare const clipMetaFile = "clip.json";
79
+ /** A span of a clip, in milliseconds from its first frame. */
80
+ export interface ClipSpan {
81
+ from: number;
82
+ to: number;
83
+ }
84
+ export declare function resolveClipsFolder(root: string): string;
85
+ /** The clip's folder, once its name is checked: nothing else reaches the
86
+ * file system with a name a request carried. */
87
+ export declare function resolveClipFolder(root: string, name: string): string;
88
+ /** A new clip's name: the local time, `YYYYMMDD-HHMMSS`, which sorts by
89
+ * when it started, with `-2`, `-3` on a clash. */
90
+ export declare function mintClipName(folder: string, at?: Date): string;
91
+ /** Every clip the game keeps, newest first; a folder that is no clip's,
92
+ * or holds no `clip.json`, is skipped. */
93
+ export declare function listClips(root: string): ListedClip[];
94
+ /** The span `from` and `to` name, in seconds from a clip's start, in
95
+ * milliseconds; none where neither is given. */
96
+ export declare function readClipSpan({ from, to }: Partial<Record<keyof ClipSpan, number>>): ClipSpan | undefined;
97
+ /** A finished clip's `clip.json`, its stills and events cut to `span`. */
98
+ export declare function readClip(root: string, name: string, span?: ClipSpan): ClipMeta;
99
+ /** The images an agent reads of a clip: its sheet, or the stills of a
100
+ * span when `readClip` cut it to one. */
101
+ export declare function readClipImages(root: string, meta: ClipMeta, span?: ClipSpan): Buffer[];
102
+ /** Removes a clip's folder; returns its meta, which names the saved replay
103
+ * window to remove beside it. */
104
+ export declare function removeClip(root: string, name: string): ClipMeta | undefined;
105
+ /** One line a clip: name, length, size, shape, events and folder, as
106
+ * `spawnite clip list` and the MCP's list_clips print them. */
107
+ export declare function describeClips(clips: ListedClip[]): string[];
108
+ /** A clip as `spawnite clip show` and the MCP's read_clip print it: what it
109
+ * is, the replay window it keeps, its sheet, and the events and stills of
110
+ * `meta`, which `readClip` cut to a span where one was asked for. */
111
+ export declare function describeClip(meta: ClipMeta, folder: string): string[];
112
+ export declare const devServerPluginServersFolder: string[];
113
+ /** The server a file in `.spawnite/servers` names: its port, the file's
114
+ * name, and the process that wrote it. */
115
+ export interface ServerFileOwner {
116
+ port: number;
117
+ pid: number;
118
+ }
119
+ /** Removes the file the dev server `owner` wrote, with each folder that
120
+ * leaves empty. A file another process wrote on that port since stays. A
121
+ * server killed outright, as the cli ends the ones it starts on Windows,
122
+ * never removes its own, so the process that killed it does. */
123
+ export declare function removeServerFile(root: string, { port, pid }: ServerFileOwner): void;
124
+ /** The chat folders of every game in a session, read as one inbox: what
125
+ * the MCP's read_chat and its channel read. */
126
+ export declare class ChatInbox {
127
+ #private;
128
+ constructor(games: string[]);
129
+ /** The unread messages, or with all every one, oldest first. Marks
130
+ * none read. */
131
+ list(options?: {
132
+ all?: boolean;
133
+ }): StoredMessage[];
134
+ /** The unread messages, each marked read as it is taken. */
135
+ take(): StoredMessage[];
136
+ /** Calls back once for each unread message, the ones already there
137
+ * first, then each that lands, leaving it unread. Returns what stops
138
+ * the watch. */
139
+ watch(onMessage: (message: StoredMessage) => void): () => void;
140
+ /** The unread messages, taken; with none, the first to land within the
141
+ * time, or nothing. An abort takes nothing, so a message is never
142
+ * marked read for a caller that has gone. */
143
+ wait(milliseconds: number, signal?: AbortSignal): Promise<StoredMessage[]>;
144
+ }
145
+ /** The hooks that serve `loadInstalledModule`'s one-line modules, for a
146
+ * server's plugins.
147
+ *
148
+ * Vite's loader takes a bare name loaded with no importer, such as
149
+ * `@spawnite/engine/internal/cli`, into its own module graph, but hands a
150
+ * name a game's file imports to Node. Two entries of one package then run
151
+ * two copies of the files they share, each with its own module state: a
152
+ * world one copy steps runs the game's systems, whose reads check the
153
+ * other copy's. Imported from a module, the name loads as the game's
154
+ * files load it. */
155
+ export declare const installedImportHooks: {
156
+ resolveId(id: string): string | undefined;
157
+ load(id: string): string | undefined;
158
+ };
159
+ /** A module through the server's loader, with a package's name loaded as a
160
+ * game's file imports it. The server's plugins hold
161
+ * `installedImportHooks`. */
162
+ export declare function loadInstalledModule<Loaded>(server: {
163
+ ssrLoadModule: (url: string) => Promise<Loaded>;
164
+ }, url: string): Promise<Loaded>;
165
+ /** The map file's path from the game's folder, for a name that is a map's,
166
+ * and a throw for one that would reach outside `src/maps`. */
167
+ export declare function locateMapJson(map: string): string;
168
+ /** A value as the repo's prettier formats JSON, starting at `column` on
169
+ * a line that ends `trail` characters after it: an object a key to a
170
+ * line, an array on one line where it fits and a value to a line where
171
+ * it does not, so a terrain grid is one row to a line. */
172
+ export declare function formatMapJson(value: unknown, depth?: number, column?: number, trail?: number): string;
173
+ /** A write made against an older revision of the map file than its own. */
174
+ export declare class MapRevisionError extends Error {}
175
+ export interface MapFileUpdate<Result> {
176
+ /** The game's folder. */
177
+ root: string;
178
+ /** The map's name, never a path: the file is `src/maps/<map>.json`. */
179
+ map: string;
180
+ /** The top-level key this writer owns, such as `terrain` or `props`. */
181
+ key: string;
182
+ /** The map's revision the caller last read; a file already past it is
183
+ * not overwritten. Left out, the write goes on top of whatever is
184
+ * there. */
185
+ revision?: number;
186
+ /** The map to write where the game has no file for it yet, such as
187
+ * the engine's meadow; left out, a missing file is refused. */
188
+ create?: () => object;
189
+ /** Given the map as the file holds it now, the key's new value and
190
+ * what the caller gets back. It runs again when another process
191
+ * wrote the file while it ran. */
192
+ update: (map: Record<string, unknown>) => {
193
+ value: unknown;
194
+ result: Result;
195
+ };
196
+ }
197
+ export interface MapFileWrite<Result> {
198
+ /** The file, from the game's folder. */
199
+ file: string;
200
+ /** The file as written. */
201
+ text: string;
202
+ /** The map's revision the write left. */
203
+ revision: number;
204
+ result: Result;
205
+ }
206
+ /** Rewrites one key of a map file: rereads the file, refuses a write made
207
+ * against an older revision than its own, replaces only that key and
208
+ * counts the write in `revision`, checks the whole map with the schema, and writes it beside and
209
+ * renames it over the old one, so a crash never leaves half a file.
210
+ * Writes to one map queue one at a time in a process, and hold a lock
211
+ * in the game's `.spawnite/maps` across processes; a write whose file a hand edit
212
+ * changed meanwhile is refused and made again on the new file. */
213
+ export declare function updateMapFile<Result>({ root, map, key, revision, create, update }: MapFileUpdate<Result>): Promise<MapFileWrite<Result>>;
214
+ interface GroundPosition {
215
+ x: number;
216
+ z: number;
217
+ }
218
+ /** A scattered instance, named for what the writer reads. */
219
+ interface ScatterPlacement {
220
+ kind: string;
221
+ position: GroundPosition;
222
+ }
223
+ /** The engine's ground surface, named for what the writer reads. */
224
+ interface GroundSurface {
225
+ map: TerrainMap;
226
+ baseHeights: ArrayLike<number>;
227
+ offsets: Float64Array;
228
+ getRegionWeightsAt: (position: GroundPosition) => number[];
229
+ }
230
+ /** The engine as the writer calls it, loaded through the game's own Vite,
231
+ * so the brush is the game's engine's. A util may not depend on the
232
+ * engine, so it is named here only for what the writer calls. */
233
+ export interface TerrainEngine {
234
+ readMap: (name: string) => TerrainMap;
235
+ createGroundSurface: (values: unknown) => GroundSurface;
236
+ applyTerrainStroke: (grid: HeightGrid, stroke: TerrainStroke) => TerrainChange;
237
+ restoreTerrain: (grid: HeightGrid, restore: TerrainRestore) => TerrainChange;
238
+ applyPaintStroke: (grid: PaintGrid, stroke: TerrainStroke, baseOf: (cell: number) => string) => TerrainChange;
239
+ fillTerrain: (grids: {
240
+ paint: PaintGrid;
241
+ height: HeightGrid;
242
+ }, fill: TerrainFill, weighRegion: (point: readonly [number, number]) => number) => TerrainChange;
243
+ restorePaint: (grid: PaintGrid, restore: TerrainRestore) => TerrainChange;
244
+ pruneErasedScatter: (surface: GroundSurface) => MapTerrain["erased"];
245
+ placeScatter: (surface: GroundSurface) => ScatterPlacement[];
246
+ eraseScatter: (grid: PaintGrid, stroke: TerrainStroke, placements: readonly ScatterPlacement[]) => TerrainChange;
247
+ resolveGroundMaterials: (surface: GroundSurface, paint?: PaintGrid) => {
248
+ names: string[];
249
+ ruled: Uint8Array;
250
+ };
251
+ }
252
+ export interface TerrainEditOptions {
253
+ /** The game's folder. */
254
+ root: string;
255
+ /** The map's name, never a path: the file is `src/maps/<map>.json`. */
256
+ map: string;
257
+ engine: TerrainEngine;
258
+ /** The map's revision the caller last read; a file already past it is
259
+ * not overwritten. Left out, the write goes on top of whatever is
260
+ * there. */
261
+ revision?: number;
262
+ stroke?: TerrainStroke;
263
+ fill?: TerrainFill;
264
+ restore?: TerrainRestoreInput;
265
+ }
266
+ export interface TerrainWrite {
267
+ /** The file, from the game's folder. */
268
+ file: string;
269
+ /** The map's revision the write left. */
270
+ revision: number;
271
+ change: TerrainChange;
272
+ /** The block as written, so the editor shows what the disk holds. */
273
+ terrain: MapTerrain;
274
+ /** The whole file as written, which the dev server knows as its own. */
275
+ text: string;
276
+ }
277
+ /** A write refused because the map's base moved under edited points
278
+ * since the terrain's last write: `keepTerrainHeights` or `moveTerrainWithGround`
279
+ * settles it first. */
280
+ export declare class TerrainBaseMovedError extends Error {}
281
+ /** The map's terrain block as its file holds it, null where the game has
282
+ * no file for the map or no brush has edited it, and the file's
283
+ * revision, 0 before its first write. */
284
+ export declare function readMapFile(root: string, map: string): {
285
+ terrain: MapTerrain | null;
286
+ revision: number;
287
+ };
288
+ /** Runs one stroke or fill, or writes an undo's values back, on the
289
+ * map's terrain block, and returns what it touched. */
290
+ export declare function editTerrain({ root, map, engine, revision, stroke, fill, restore }: TerrainEditOptions): Promise<TerrainWrite>;
291
+ export interface TerrainRewrite {
292
+ file: string;
293
+ revision: number;
294
+ /** Points that hold an edit after the rewrite. */
295
+ edited: number;
296
+ }
297
+ /** Rewrites the offsets so every edited point keeps the height it had at
298
+ * the last write, over the map's base as it stands now: the block's copy
299
+ * of the old base bakes the old ground. */
300
+ export declare function keepTerrainHeights({ root, map, engine, revision }: Pick<TerrainEditOptions, "root" | "map" | "engine" | "revision">): Promise<TerrainRewrite>;
301
+ /** Records the map's base as it stands now under the block's edits, so
302
+ * every edited point keeps its offset and rides on the new base. */
303
+ export declare function moveTerrainWithGround({ root, map, engine, revision }: Pick<TerrainEditOptions, "root" | "map" | "engine" | "revision">): Promise<TerrainRewrite>;
304
+ /** Rewrites the block at its map's grid, where the map's size or cell
305
+ * changed: each new point takes the offset of the old point nearest to
306
+ * it, and a point past the old grid's edge is untouched. */
307
+ export declare function resampleTerrain({ root, map, engine }: Pick<TerrainEditOptions, "root" | "map" | "engine">): Promise<TerrainRewrite>;
308
+ export interface MapMaterialWrite {
309
+ /** The file, from the game's folder. */
310
+ file: string;
311
+ /** The file as written. */
312
+ text: string;
313
+ /** The map's revision the write left. */
314
+ revision: number;
315
+ /** The map's `materials` as written. */
316
+ materials: Materials;
317
+ }
318
+ /** Writes one entry of the map's `materials`: its fields over the entry
319
+ * the file holds, so a tint leaves the tile as it was, and a new name
320
+ * with `from` makes a new material, as the editor's Duplicate does. The
321
+ * terrain and every other key stay as they are. */
322
+ export declare function writeMapMaterial({ root, map, engine, name, look, whole, revision }: Pick<TerrainEditOptions, "root" | "map" | "revision"> & {
323
+ engine: Pick<TerrainEngine, "readMap">;
324
+ name: string;
325
+ /** Null takes the entry out. */
326
+ look: MaterialLook | null;
327
+ /** Writes `look` as the whole entry, as an undo does, in place of
328
+ * over the file's field by field. */
329
+ whole?: boolean;
330
+ }): Promise<MapMaterialWrite>;
331
+ export interface MapSeaWrite {
332
+ /** The file, from the game's folder. */
333
+ file: string;
334
+ /** The file as written. */
335
+ text: string;
336
+ /** The map's revision the write left. */
337
+ revision: number;
338
+ /** The sea's height as written. */
339
+ sea: number;
340
+ }
341
+ /** Writes the map's sea height, as the editor's Sea field and `spawnite
342
+ * map terrain sea` set it. */
343
+ export declare function writeMapSea({ root, map, engine, level, revision }: Pick<TerrainEditOptions, "root" | "map" | "revision"> & {
344
+ engine: Pick<TerrainEngine, "readMap">;
345
+ level: number;
346
+ }): Promise<MapSeaWrite>;
347
+ /** The map's props with the patch's upserted and its null ones removed,
348
+ * and the names it removed. */
349
+ export declare function mergeMapProps(current: unknown, patch: Record<string, object | null>): {
350
+ merged: Record<string, object>;
351
+ removed: string[];
352
+ };
353
+ export interface MapPropsWrite {
354
+ /** The file, from the game's folder. */
355
+ file: string;
356
+ /** The file as written. */
357
+ text: string;
358
+ /** The map's revision the write left. */
359
+ revision: number;
360
+ /** The props the patch named as null that the file held. */
361
+ removed: string[];
362
+ }
363
+ /** Writes props into the map's `props`, merged on the file as it stands
364
+ * at the write: each named one upserted and each null one removed, the
365
+ * rest and every other key as they were. The editor's placing and the
366
+ * cli's `map place` both write through it. An engine map with no file,
367
+ * such as meadow, is written into the game with the first. */
368
+ export declare function writeMapProps({ root, map, engine, props, revision }: Pick<TerrainEditOptions, "root" | "map" | "revision"> & {
369
+ /** Left out, a map with no file is refused rather than written. */
370
+ engine?: Pick<TerrainEngine, "readMap">;
371
+ props: Record<string, object | null>;
372
+ }): Promise<MapPropsWrite>;
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ import{A as e,C as t,D as n,E as r,F as i,I as a,L as o,M as s,N as c,O as l,P as u,S as d,T as f,_ as p,a as m,b as h,c as g,d as _,f as v,g as y,h as b,i as x,j as S,k as C,l as w,m as T,n as E,o as D,p as O,r as k,s as A,t as j,u as M,v as N,w as P,x as F,y as I}from"./BlZU7Y-r.js";import{i as L,n as R,r as z,t as B}from"./BeeXEexm.js";import{setTimeout as V}from"node:timers/promises";var H=250,U=class{#e;constructor(e){this.#e=e}list(t={}){return this.#e.flatMap(n=>e(n,t)).sort(l)}take(){return this.list().flatMap(e=>{try{return[{...e,file:s(e.file)}]}catch{return[]}})}watch(e){let t=/* @__PURE__ */ new Set,n=setInterval(()=>{let n;try{n=this.list()}catch{return}for(let r of n)t.has(r.file)||(t.add(r.file),e(r))},H);return()=>clearInterval(n)}async wait(e,t){let n=Date.now()+e;for(;!t?.aborted;){let e=this.take();if(e.length>0||Date.now()>=n)return e;await V(H,void 0,{signal:t}).catch(()=>void 0)}return[]}};export{U as ChatInbox,v as MapRevisionError,E as TerrainBaseMovedError,y as clipMetaFile,p as clipVideoFile,l as compareMessages,j as createDevServerApp,N as describeClip,I as describeClips,z as devServerPluginServersFolder,k as editTerrain,O as formatMapJson,C as inlineShots,B as installedImportHooks,x as keepTerrainHeights,h as listClips,e as listMessages,S as listServers,R as loadInstalledModule,T as locateMapJson,s as markRead,m as mergeMapProps,F as mintClipName,D as moveTerrainWithGround,d as readClip,t as readClipImages,P as readClipSpan,A as readMapFile,f as removeClip,L as removeServerFile,c as renderMessage,g as resampleTerrain,u as resolveChatFolder,r as resolveClipFolder,n as resolveClipsFolder,i as resolveShotsFolder,b as updateMapFile,w as writeMapMaterial,M as writeMapProps,_ as writeMapSea,a as writeMessage,o as writeShot};
@@ -0,0 +1 @@
1
+ import{t as e}from"./BlZU7Y-r.js";import{getRequestListener as t}from"@hono/node-server";function n(n){let r=t(e(n).fetch,{overrideGlobalObjects:!1});return(e,t,n)=>{r(e,t).catch(n)}}export{n as createDevServerMiddleware};
@@ -0,0 +1 @@
1
+ function e(e){if(!e)return;let t=crypto.randomUUID(),n=`spawnite:tab`,r=sessionStorage.getItem(n)??crypto.randomUUID();sessionStorage.removeItem(n),addEventListener(`pagehide`,()=>sessionStorage.setItem(n,r));let i=Date.now(),a=()=>{let n=document.visibilityState===`visible`;n&&(i=Date.now()),e.send(`spawnite:tab`,{id:r,href:location.href,load:t,visible:n,automated:navigator.webdriver===!0,seenAt:i})};a(),e.on(`vite:ws:connect`,async()=>a()),e.on(`spawnite:terrain`,async e=>{dispatchEvent(new CustomEvent(`spawnite:terrain`,{detail:e}))}),addEventListener(`focus`,a),document.addEventListener(`visibilitychange`,a);let o=[],s=e=>{o.push(e),o.length>1e3&&o.shift()},c=!1;window.__SPAWNITE_TAB__={tapConsole:()=>{if(!c){c=!0;for(let e of[`log`,`info`,`warn`,`error`,`debug`]){let t=console[e];console[e]=(...n)=>{s(`${e}: ${n.map(String).join(` `)}`),t.apply(console,n)}}addEventListener(`error`,e=>s(`pageerror: ${e.message}`)),addEventListener(`unhandledrejection`,e=>s(`pageerror: ${String(e.reason)}`))}},takeConsole:()=>o.splice(0)},e.on(`spawnite:run`,async({id:n,expression:r})=>{try{let i=await(0,eval)(r);e.send(`spawnite:ran`,{id:n,load:t,value:i})}catch(r){e.send(`spawnite:ran`,{id:n,load:t,error:r instanceof Error?r.message:String(r)})}})}export{e as runTabClient};
package/dist/vite.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { Plugin } from "vite";
2
+ export declare const devServerPluginPrefix = "/__";
3
+ /** Every dev-server route in a game's Vite server, beside the engine's
4
+ * spawnite(), and the client in each page it serves that lets the agent's
5
+ * tools reach the creator's own tab. */
6
+ export declare function devServer(): Plugin;
7
+ /** The address Vite serves the file at `path` from, outside the root:
8
+ * `/@fs/` and the path in forward slashes, a Windows drive's among them,
9
+ * as Vite's own `normalizePath` writes it. */
10
+ export declare function readFsUrl(path: string): string;
package/dist/vite.js ADDED
@@ -0,0 +1 @@
1
+ import{i as e,n as t,r as n,t as r}from"./BeeXEexm.js";import{basename as i,extname as a,join as o}from"node:path";import{fileURLToPath as s}from"node:url";import{mkdirSync as c,readFileSync as l,renameSync as u,writeFileSync as d}from"node:fs";import{randomUUID as f}from"node:crypto";function p(e){try{return JSON.parse(e)}catch{return}}function m({held:e,wrote:t,next:n}){if(n===t)return`keep`;let r=p(n);if(!r)return`wait`;let i=e===void 0?void 0:p(e);if(!i)return`reload`;let{terrain:a,revision:o,...s}=i,{terrain:c,revision:l,...u}=r;return JSON.stringify(s)===JSON.stringify(u)?JSON.stringify(a)===JSON.stringify(c)?`keep`:c===void 0?`reload`:`terrain`:`reload`}var h={hello:`spawnite:tab`,run:`spawnite:run`,ran:`spawnite:ran`};function g(e){return typeof e==`object`&&!!e}function _(e){return g(e)&&typeof e.id==`string`&&typeof e.href==`string`&&typeof e.load==`string`&&typeof e.visible==`boolean`&&typeof e.automated==`boolean`&&typeof e.seenAt==`number`}function v(e){return g(e)&&typeof e.id==`string`&&typeof e.load==`string`}function y(e){let t=/* @__PURE__ */ new Map,n=/* @__PURE__ */ new Map,r=e=>{let t=n.get(e);return n.delete(e),t&&clearTimeout(t.timer),t};e.on(h.hello,(e,n)=>{if(_(e)){for(let[r,i]of t)r!==n&&i.id===e.id&&t.delete(r);t.set(n,e)}}),e.on(h.ran,(e,n)=>{if(!v(e))return;let i=r(e.id),a=t.get(n);if(!i||!a)return;let o={...a,load:e.load};t.set(n,o),e.error===void 0?i.resolve({tab:o,value:e.value}):i.reject(Error(e.error))}),e.on(`vite:ws:disconnect`,(e,i)=>{t.delete(i);for(let[e,t]of n)t.client===i&&r(e)?.reject(/* @__PURE__ */ Error(`The tab closed or reloaded before it answered`))});let i=e=>{if(e!==void 0){let n=[...t].find(([,t])=>t.id===e);if(!n)throw Error(`The tab the agent attached to is gone: it closed, or its dev server restarted and the page has not joined again. Open the game, then attach again`);return n}let n=[...t].filter(([,e])=>!e.automated);if(n.length===0)throw Error(`No tab of the game is open: open the dev server's address in your browser`);let r=n.filter(([,e])=>e.visible);return(r.length>0?r:n).reduce((e,t)=>t[1].seenAt>e[1].seenAt?t:e)};return{list:()=>[...t.values()],run:(e,t,a)=>new Promise((o,s)=>{let[c]=i(a),l=f(),u=setTimeout(()=>r(l)?.reject(/* @__PURE__ */ Error(`The tab did not answer within ${t/1e3} s: it may have reloaded, or a step waits on a frame it never drew`)),t);n.set(l,{client:c,resolve:o,reject:s,timer:u}),c.send(h.run,{id:l,expression:e})})}}var b=`/__`;function x(){let e,t=t=>e!==void 0&&t.startsWith(e)&&t.endsWith(`.json`)&&!t.slice(e.length).includes(`/`),n=/* @__PURE__ */ new Map,o=/* @__PURE__ */ new Map;return{name:`spawnite:dev-server`,configResolved(t){e=`${t.root}/src/maps/`},resolveId:r.resolveId,load(e){let i=e.replace(/\?.*$/,``);return t(i)&&n.set(`${this.environment.name} ${i}`,l(i,`utf8`)),r.load(e)},hotUpdate:{order:`post`,async handler({type:e,file:r,modules:a,read:s,server:c}){if(!t(r))return;let l=`${this.environment.name} ${r}`;if(e===`delete`){n.delete(l);return}let u=await s(),d=m({held:n.get(l),wrote:o.get(r),next:u});if(d===`reload`)return a.length>0||this.environment.name!==`client`?void 0:(c.ws.send({type:`full-reload`}),[]);if(d===`wait`)return[];n.set(l,u);for(let e of a)this.environment.moduleGraph.invalidateModule(e);return d===`terrain`&&this.environment.name===`client`&&c.ws.send({type:`custom`,event:`spawnite:terrain`,data:{map:i(r,`.json`),revision:JSON.parse(u).revision}}),[]}},configureServer(e){let t=f(),n=y(e.ws);C(e,t);let r;e.middlewares.use((i,a,s)=>{if(!i.url?.startsWith(`/__`))return s();r??=w(e,{tabs:n,token:t,noteMapWrite:(e,t)=>o.set(e,t)}),r.then(e=>e(i,a,s)).catch(e=>{r=void 0,s(e)})})},transformIndexHtml:{order:`pre`,handler:(e,{server:t})=>{if(!t)return[];let n=s(new URL(`./tabClient${a(import.meta.url)}`,import.meta.url));return[{tag:`script`,attrs:{type:`module`},children:`import { runTabClient } from ${JSON.stringify(S(n))};\nrunTabClient(import.meta.hot);`,injectTo:`head`}]}}}}function S(e){return`/@fs/${e.replaceAll(`\\`,`/`).replace(/^\/+/,``)}`}function C(t,r){let{httpServer:i}=t;i?.once(`listening`,()=>{let a=i.address();if(a===null||typeof a==`string`)return;let s=o(t.config.root,...n),l=o(s,`${a.port}.json`);c(s,{recursive:!0}),d(`${l}.tmp`,JSON.stringify({url:`http://localhost:${a.port}/`,token:r,pid:process.pid}),{mode:384}),u(`${l}.tmp`,l),i.once(`close`,()=>e(t.config.root,{port:a.port,pid:process.pid}))})}async function w(e,{tabs:n,token:r,noteMapWrite:i}){let o=s(new URL(`./middleware${a(import.meta.url)}`,import.meta.url)),c=e.config.plugins.find(({name:e})=>e===`spawnite`)?.api,{createDevServerMiddleware:l}=await e.ssrLoadModule(o);return l({root:e.config.root,warn:t=>e.config.logger.warn(t),tabs:n,token:r,loadModule:n=>t(e,n),noteMapWrite:i,clips:c?.clips})}export{x as devServer,b as devServerPluginPrefix,S as readFsUrl};
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@spawnite/dev-server",
3
+ "description": "The routes a game's Vite dev server answers for the devtools page and the agent's tools: screenshots, chat and the creator's own tab.",
4
+ "version": "0.1.0-beta.15",
5
+ "license": "SEE LICENSE IN LICENSE.md",
6
+ "homepage": "https://wiki.spawnite.com",
7
+ "publishConfig": {
8
+ "registry": "https://registry.npmjs.org",
9
+ "access": "public"
10
+ },
11
+ "engines": {
12
+ "node": ">=24.2"
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "LICENSE.md",
17
+ "!dist/*.tsbuildinfo",
18
+ "!dist/types",
19
+ "!dist/**/*.map"
20
+ ],
21
+ "type": "module",
22
+ "main": "./dist/index.js",
23
+ "module": "./dist/index.js",
24
+ "types": "./dist/index.d.ts",
25
+ "exports": {
26
+ "./package.json": "./package.json",
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "import": "./dist/index.js",
30
+ "default": "./dist/index.js"
31
+ },
32
+ "./vite": {
33
+ "types": "./dist/vite.d.ts",
34
+ "import": "./dist/vite.js",
35
+ "default": "./dist/vite.js"
36
+ }
37
+ },
38
+ "dependencies": {
39
+ "@hono/node-server": "2.1.1",
40
+ "@hono/zod-validator": "0.9.1",
41
+ "@modelcontextprotocol/sdk": "1.30.1",
42
+ "hono": "4.13.9",
43
+ "jsonc-parser": "3.3.1",
44
+ "proper-lockfile": "4.1.2",
45
+ "zod": "4.6.5",
46
+ "@spawnite/schema": "0.1.0-beta.15"
47
+ },
48
+ "peerDependencies": {
49
+ "vite": "^8.1.0"
50
+ },
51
+ "peerDependenciesMeta": {
52
+ "vite": {
53
+ "optional": true
54
+ }
55
+ }
56
+ }