modwright 0.1.0

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.
Files changed (455) hide show
  1. package/LICENSE +35 -0
  2. package/README.md +212 -0
  3. package/assets/claude/agents/mw-explore.md +46 -0
  4. package/assets/claude/agents/mw-fixture.md +33 -0
  5. package/assets/claude/agents/mw-rule-author.md +51 -0
  6. package/assets/claude/agents/mw-scaffold-author.md +48 -0
  7. package/assets/claude/skills/mw-author.md +59 -0
  8. package/assets/claude/skills/mw-new-item.md +51 -0
  9. package/assets/claude/skills/mw-new-spell.md +50 -0
  10. package/assets/claude/skills/mw-scaffold.md +59 -0
  11. package/assets/claude/skills/mw-ship.md +57 -0
  12. package/assets/claude/skills/mw-validate.md +51 -0
  13. package/assets/claude/skills/mw-verify.md +59 -0
  14. package/assets/nivalisnights/extract_resources.py +177 -0
  15. package/bridges/bg3-se/Mods/ModWrightBridge/ScriptExtender/Config.json +5 -0
  16. package/bridges/bg3-se/Mods/ModWrightBridge/ScriptExtender/Lua/BootstrapClient.lua +55 -0
  17. package/bridges/bg3-se/Mods/ModWrightBridge/ScriptExtender/Lua/BootstrapServer.lua +81 -0
  18. package/bridges/bg3-se/Mods/ModWrightBridge/ScriptExtender/Lua/ModWrightBridge/Log.lua +96 -0
  19. package/bridges/bg3-se/Mods/ModWrightBridge/ScriptExtender/Lua/ModWrightBridge/Probes.lua +1038 -0
  20. package/bridges/bg3-se/Mods/ModWrightBridge/ScriptExtender/Lua/ModWrightBridge/ProbesClient.lua +74 -0
  21. package/bridges/bg3-se/Mods/ModWrightBridge/ScriptExtender/Lua/ModWrightBridge/Protocol.lua +985 -0
  22. package/bridges/bg3-se/Mods/ModWrightBridge/ScriptExtender/Lua/ModWrightBridge/Watch.lua +175 -0
  23. package/bridges/bg3-se/Mods/ModWrightBridge/meta.lsx +45 -0
  24. package/bridges/bg3-se/README.md +99 -0
  25. package/bridges/bg3-se/bridge.json +68 -0
  26. package/bridges/cp2077-cet/ModWrightBridge/init.lua +375 -0
  27. package/bridges/cp2077-cet/ModWrightBridge/probes.lua +1104 -0
  28. package/bridges/cp2077-cet/ModWrightBridge/protocol.lua +1306 -0
  29. package/bridges/cp2077-cet/README.md +165 -0
  30. package/bridges/cp2077-cet/bridge.json +53 -0
  31. package/dist/core/bridge/arm.js +175 -0
  32. package/dist/core/bridge/cet-console.js +26 -0
  33. package/dist/core/bridge/cet-log.js +362 -0
  34. package/dist/core/bridge/index.js +11 -0
  35. package/dist/core/bridge/locate.js +35 -0
  36. package/dist/core/bridge/paths.js +34 -0
  37. package/dist/core/bridge/pending.js +89 -0
  38. package/dist/core/bridge/poll.js +155 -0
  39. package/dist/core/bridge/stage.js +100 -0
  40. package/dist/core/bridge/status.js +97 -0
  41. package/dist/core/bridge/types.js +145 -0
  42. package/dist/core/bridge/validate.js +107 -0
  43. package/dist/core/build/index.js +6 -0
  44. package/dist/core/build/manifest.js +47 -0
  45. package/dist/core/build/paths.js +32 -0
  46. package/dist/core/build/run.js +342 -0
  47. package/dist/core/build/stage.js +265 -0
  48. package/dist/core/build/steps/bg3.js +129 -0
  49. package/dist/core/build/steps/core.js +195 -0
  50. package/dist/core/build/steps/cyberpunk.js +95 -0
  51. package/dist/core/build/steps/listing.js +35 -0
  52. package/dist/core/build/types.js +13 -0
  53. package/dist/core/build/zip.js +148 -0
  54. package/dist/core/compat/check.js +113 -0
  55. package/dist/core/compat/pe.js +220 -0
  56. package/dist/core/compat/types.js +1 -0
  57. package/dist/core/compat/version.js +70 -0
  58. package/dist/core/deploy/guards.js +292 -0
  59. package/dist/core/deploy/index.js +5 -0
  60. package/dist/core/deploy/plan.js +696 -0
  61. package/dist/core/deploy/reload-apply.js +361 -0
  62. package/dist/core/deploy/reload.js +358 -0
  63. package/dist/core/deploy/sources.js +169 -0
  64. package/dist/core/deploy/types.js +1 -0
  65. package/dist/core/epic.js +54 -0
  66. package/dist/core/fsutil.js +191 -0
  67. package/dist/core/home.js +70 -0
  68. package/dist/core/knowledge/facts.js +229 -0
  69. package/dist/core/knowledge/index.js +4 -0
  70. package/dist/core/knowledge/search.js +112 -0
  71. package/dist/core/knowledge/store.js +123 -0
  72. package/dist/core/knowledge/types.js +1 -0
  73. package/dist/core/ledger/check.js +42 -0
  74. package/dist/core/ledger/hooks.js +80 -0
  75. package/dist/core/ledger/index.js +7 -0
  76. package/dist/core/ledger/query.js +17 -0
  77. package/dist/core/ledger/record.js +84 -0
  78. package/dist/core/ledger/status.js +53 -0
  79. package/dist/core/ledger/store.js +98 -0
  80. package/dist/core/ledger/types.js +108 -0
  81. package/dist/core/logs/index.js +4 -0
  82. package/dist/core/logs/registry.js +50 -0
  83. package/dist/core/logs/scan.js +195 -0
  84. package/dist/core/logs/triage.js +160 -0
  85. package/dist/core/logs/types.js +1 -0
  86. package/dist/core/lsx.js +10 -0
  87. package/dist/core/lz4.js +66 -0
  88. package/dist/core/mods.js +229 -0
  89. package/dist/core/process.js +49 -0
  90. package/dist/core/project/build-steps.js +83 -0
  91. package/dist/core/project/describe.js +74 -0
  92. package/dist/core/project/index.js +5 -0
  93. package/dist/core/project/load.js +146 -0
  94. package/dist/core/project/schema.js +264 -0
  95. package/dist/core/project/types.js +3 -0
  96. package/dist/core/prompts.js +72 -0
  97. package/dist/core/registry.js +35 -0
  98. package/dist/core/safety/backup.js +206 -0
  99. package/dist/core/safety/execute.js +188 -0
  100. package/dist/core/safety/index.js +5 -0
  101. package/dist/core/safety/plan.js +122 -0
  102. package/dist/core/safety/restore.js +172 -0
  103. package/dist/core/safety/types.js +1 -0
  104. package/dist/core/scaffold/bootstrap.js +7 -0
  105. package/dist/core/scaffold/catalog.js +28 -0
  106. package/dist/core/scaffold/claude-assets.js +271 -0
  107. package/dist/core/scaffold/collisions.js +64 -0
  108. package/dist/core/scaffold/emit.js +356 -0
  109. package/dist/core/scaffold/engine.js +680 -0
  110. package/dist/core/scaffold/ids.js +78 -0
  111. package/dist/core/scaffold/index.js +10 -0
  112. package/dist/core/scaffold/jsonpath.js +121 -0
  113. package/dist/core/scaffold/load.js +81 -0
  114. package/dist/core/scaffold/render.js +85 -0
  115. package/dist/core/scaffold/sequence.js +396 -0
  116. package/dist/core/scaffold/types.js +309 -0
  117. package/dist/core/steam.js +63 -0
  118. package/dist/core/surfaceutil.js +36 -0
  119. package/dist/core/testplan/check.js +67 -0
  120. package/dist/core/testplan/index.js +5 -0
  121. package/dist/core/testplan/load.js +100 -0
  122. package/dist/core/testplan/registry.js +317 -0
  123. package/dist/core/testplan/run.js +389 -0
  124. package/dist/core/testplan/types.js +82 -0
  125. package/dist/core/text/converters/bg3.js +50 -0
  126. package/dist/core/text/converters/cyberpunk.js +36 -0
  127. package/dist/core/text/converters/index.js +261 -0
  128. package/dist/core/text/diff/cr2w.js +180 -0
  129. package/dist/core/text/diff/index.js +16 -0
  130. package/dist/core/text/diff/loca.js +43 -0
  131. package/dist/core/text/diff/lsx.js +126 -0
  132. package/dist/core/text/handles.js +86 -0
  133. package/dist/core/text/index.js +6 -0
  134. package/dist/core/text/json.js +46 -0
  135. package/dist/core/text/templates/cache.js +71 -0
  136. package/dist/core/text/templates/index.js +3 -0
  137. package/dist/core/text/templates/library.js +245 -0
  138. package/dist/core/text/templates/types.js +51 -0
  139. package/dist/core/text/toolchain.js +16 -0
  140. package/dist/core/text/types.js +1 -0
  141. package/dist/core/text/validate.js +149 -0
  142. package/dist/core/text/xml.js +58 -0
  143. package/dist/core/toolchain/datafetch.js +151 -0
  144. package/dist/core/toolchain/env.js +63 -0
  145. package/dist/core/toolchain/features.js +170 -0
  146. package/dist/core/toolchain/index.js +8 -0
  147. package/dist/core/toolchain/install.js +191 -0
  148. package/dist/core/toolchain/locate.js +275 -0
  149. package/dist/core/toolchain/remember.js +84 -0
  150. package/dist/core/toolchain/run.js +318 -0
  151. package/dist/core/toolchain/specs.js +351 -0
  152. package/dist/core/toolchain/types.js +1 -0
  153. package/dist/core/types.js +1 -0
  154. package/dist/core/userconfig.js +66 -0
  155. package/dist/core/validate/context.js +139 -0
  156. package/dist/core/validate/index.js +5 -0
  157. package/dist/core/validate/registry.js +58 -0
  158. package/dist/core/validate/run.js +131 -0
  159. package/dist/core/validate/suppressions.js +91 -0
  160. package/dist/core/validate/types.js +2 -0
  161. package/dist/core/yamledit.js +51 -0
  162. package/dist/index.js +389 -0
  163. package/dist/server.js +1914 -0
  164. package/dist/surfaces/baldursgate3/compat.js +164 -0
  165. package/dist/surfaces/baldursgate3/identity.js +110 -0
  166. package/dist/surfaces/baldursgate3/index/build.js +331 -0
  167. package/dist/surfaces/baldursgate3/index/index.js +17 -0
  168. package/dist/surfaces/baldursgate3/index/info.js +40 -0
  169. package/dist/surfaces/baldursgate3/index/parsers/loca.js +41 -0
  170. package/dist/surfaces/baldursgate3/index/parsers/lsx.js +137 -0
  171. package/dist/surfaces/baldursgate3/index/parsers/stats.js +45 -0
  172. package/dist/surfaces/baldursgate3/index/parsers/treasuretable.js +57 -0
  173. package/dist/surfaces/baldursgate3/index/parsers/xmlutil.js +142 -0
  174. package/dist/surfaces/baldursgate3/index/queries.js +327 -0
  175. package/dist/surfaces/baldursgate3/index/schema.js +62 -0
  176. package/dist/surfaces/baldursgate3/index.js +3 -0
  177. package/dist/surfaces/baldursgate3/logs.js +278 -0
  178. package/dist/surfaces/baldursgate3/modsettings.js +115 -0
  179. package/dist/surfaces/baldursgate3/pak.js +153 -0
  180. package/dist/surfaces/baldursgate3/settings.js +49 -0
  181. package/dist/surfaces/baldursgate3/surface.js +232 -0
  182. package/dist/surfaces/baldursgate3/validators/bridge.js +7 -0
  183. package/dist/surfaces/baldursgate3/validators/files.js +476 -0
  184. package/dist/surfaces/baldursgate3/validators/items.js +1001 -0
  185. package/dist/surfaces/baldursgate3/validators/progression.js +734 -0
  186. package/dist/surfaces/baldursgate3/validators/stats.js +1076 -0
  187. package/dist/surfaces/baldursgate3/validators/story.js +790 -0
  188. package/dist/surfaces/baldursgate3/validators/templates.js +7 -0
  189. package/dist/surfaces/cyberpunk2077/compat.js +223 -0
  190. package/dist/surfaces/cyberpunk2077/index/build.js +231 -0
  191. package/dist/surfaces/cyberpunk2077/index/index.js +17 -0
  192. package/dist/surfaces/cyberpunk2077/index/info.js +51 -0
  193. package/dist/surfaces/cyberpunk2077/index/model.js +52 -0
  194. package/dist/surfaces/cyberpunk2077/index/parsers/tweak.js +483 -0
  195. package/dist/surfaces/cyberpunk2077/index/parsers/tweakxl-yaml.js +183 -0
  196. package/dist/surfaces/cyberpunk2077/index/queries.js +504 -0
  197. package/dist/surfaces/cyberpunk2077/index/resource.js +135 -0
  198. package/dist/surfaces/cyberpunk2077/index/schema.js +90 -0
  199. package/dist/surfaces/cyberpunk2077/index.js +3 -0
  200. package/dist/surfaces/cyberpunk2077/logs.js +742 -0
  201. package/dist/surfaces/cyberpunk2077/paths.js +12 -0
  202. package/dist/surfaces/cyberpunk2077/surface.js +218 -0
  203. package/dist/surfaces/cyberpunk2077/validators/bridge.js +7 -0
  204. package/dist/surfaces/cyberpunk2077/validators/cr2w.js +349 -0
  205. package/dist/surfaces/cyberpunk2077/validators/garments.js +1181 -0
  206. package/dist/surfaces/cyberpunk2077/validators/mesh.js +338 -0
  207. package/dist/surfaces/cyberpunk2077/validators/modsettings.js +120 -0
  208. package/dist/surfaces/cyberpunk2077/validators/packaging.js +463 -0
  209. package/dist/surfaces/cyberpunk2077/validators/red4ext.js +318 -0
  210. package/dist/surfaces/cyberpunk2077/validators/redscript.js +193 -0
  211. package/dist/surfaces/cyberpunk2077/validators/templates.js +7 -0
  212. package/dist/surfaces/cyberpunk2077/validators/tweaks.js +768 -0
  213. package/dist/surfaces/cyberpunk2077/validators/vehicles.js +531 -0
  214. package/dist/surfaces/cyberpunk2077/validators/xl.js +308 -0
  215. package/dist/surfaces/eldenring/binders.js +111 -0
  216. package/dist/surfaces/eldenring/compat.js +166 -0
  217. package/dist/surfaces/eldenring/dcx.js +49 -0
  218. package/dist/surfaces/eldenring/index.js +13 -0
  219. package/dist/surfaces/eldenring/loaders.js +132 -0
  220. package/dist/surfaces/eldenring/logs.js +67 -0
  221. package/dist/surfaces/eldenring/paramdef.js +256 -0
  222. package/dist/surfaces/eldenring/params.js +129 -0
  223. package/dist/surfaces/eldenring/surface.js +121 -0
  224. package/dist/surfaces/eldenring/tools.js +52 -0
  225. package/dist/surfaces/eldenring/validators/binders.js +193 -0
  226. package/dist/surfaces/eldenring/validators/packages.js +159 -0
  227. package/dist/surfaces/eldenring/validators/params.js +334 -0
  228. package/dist/surfaces/eldenring/validators/profiles.js +287 -0
  229. package/dist/surfaces/index.js +34 -0
  230. package/dist/surfaces/nivalisnights/compat.js +229 -0
  231. package/dist/surfaces/nivalisnights/index/build.js +310 -0
  232. package/dist/surfaces/nivalisnights/index/index.js +5 -0
  233. package/dist/surfaces/nivalisnights/index/queries.js +231 -0
  234. package/dist/surfaces/nivalisnights/index.js +4 -0
  235. package/dist/surfaces/nivalisnights/logs.js +473 -0
  236. package/dist/surfaces/nivalisnights/surface.js +195 -0
  237. package/dist/surfaces/skyrimse/archives.js +229 -0
  238. package/dist/surfaces/skyrimse/compat.js +163 -0
  239. package/dist/surfaces/skyrimse/index.js +12 -0
  240. package/dist/surfaces/skyrimse/loadorder.js +174 -0
  241. package/dist/surfaces/skyrimse/logs.js +241 -0
  242. package/dist/surfaces/skyrimse/plugins.js +149 -0
  243. package/dist/surfaces/skyrimse/records.js +94 -0
  244. package/dist/surfaces/skyrimse/surface.js +240 -0
  245. package/dist/surfaces/skyrimse/validators/archive-records.js +165 -0
  246. package/dist/surfaces/skyrimse/validators/archives.js +110 -0
  247. package/dist/surfaces/skyrimse/validators/plugins.js +374 -0
  248. package/dist/surfaces/skyrimse/validators/records.js +117 -0
  249. package/dist/surfaces/stardewvalley/compat.js +190 -0
  250. package/dist/surfaces/stardewvalley/config.js +42 -0
  251. package/dist/surfaces/stardewvalley/contentpatcher.js +203 -0
  252. package/dist/surfaces/stardewvalley/index/index.js +229 -0
  253. package/dist/surfaces/stardewvalley/index/wiki.js +190 -0
  254. package/dist/surfaces/stardewvalley/index.js +12 -0
  255. package/dist/surfaces/stardewvalley/logs.js +307 -0
  256. package/dist/surfaces/stardewvalley/mods.js +182 -0
  257. package/dist/surfaces/stardewvalley/surface.js +244 -0
  258. package/dist/surfaces/stardewvalley/validators/content.js +321 -0
  259. package/dist/surfaces/stardewvalley/validators/fields.js +147 -0
  260. package/dist/surfaces/stardewvalley/validators/loadorder.js +69 -0
  261. package/dist/surfaces/stardewvalley/validators/manifest.js +221 -0
  262. package/dist/surfaces/subnautica2/compat.js +138 -0
  263. package/dist/surfaces/subnautica2/index.js +5 -0
  264. package/dist/surfaces/subnautica2/logs.js +129 -0
  265. package/dist/surfaces/subnautica2/paks.js +173 -0
  266. package/dist/surfaces/subnautica2/surface.js +151 -0
  267. package/dist/surfaces/subnautica2/validators/packaging.js +298 -0
  268. package/dist/surfaces/valheim/compat.js +171 -0
  269. package/dist/surfaces/valheim/index/index.js +301 -0
  270. package/dist/surfaces/valheim/index/tables.js +103 -0
  271. package/dist/surfaces/valheim/index.js +11 -0
  272. package/dist/surfaces/valheim/logs.js +197 -0
  273. package/dist/surfaces/valheim/packaging.js +131 -0
  274. package/dist/surfaces/valheim/plugins.js +270 -0
  275. package/dist/surfaces/valheim/references.js +121 -0
  276. package/dist/surfaces/valheim/surface.js +223 -0
  277. package/dist/surfaces/valheim/validators/packaging.js +218 -0
  278. package/dist/surfaces/valheim/validators/plugins.js +287 -0
  279. package/dist/surfaces/valheim/validators/references.js +169 -0
  280. package/knowledge/baldursgate3/animations.gr2.yaml +166 -0
  281. package/knowledge/baldursgate3/audio.voice.yaml +182 -0
  282. package/knowledge/baldursgate3/audio.wwise.yaml +104 -0
  283. package/knowledge/baldursgate3/charactercreation.heads-hair.yaml +193 -0
  284. package/knowledge/baldursgate3/communitylibrary.shared-content.yaml +129 -0
  285. package/knowledge/baldursgate3/compatibilityframework.api.yaml +176 -0
  286. package/knowledge/baldursgate3/crash.causes.yaml +90 -0
  287. package/knowledge/baldursgate3/dialogue.dialogbank.yaml +120 -0
  288. package/knowledge/baldursgate3/dialogue.files.yaml +236 -0
  289. package/knowledge/baldursgate3/dialogue.osiris-goals.yaml +386 -0
  290. package/knowledge/baldursgate3/dialogue.yaml +122 -0
  291. package/knowledge/baldursgate3/feats.classes.yaml +164 -0
  292. package/knowledge/baldursgate3/items.cloth-physics.yaml +167 -0
  293. package/knowledge/baldursgate3/items.roottemplates.yaml +320 -0
  294. package/knowledge/baldursgate3/items.visualbank.yaml +628 -0
  295. package/knowledge/baldursgate3/journal.quests.yaml +214 -0
  296. package/knowledge/baldursgate3/loadorder.modsettings.yaml +128 -0
  297. package/knowledge/baldursgate3/localization.handles.yaml +92 -0
  298. package/knowledge/baldursgate3/misc.encoding-and-practice.yaml +111 -0
  299. package/knowledge/baldursgate3/origins.companions.yaml +144 -0
  300. package/knowledge/baldursgate3/osiris.signatures.yaml +211 -0
  301. package/knowledge/baldursgate3/packaging.meta.yaml +155 -0
  302. package/knowledge/baldursgate3/packaging.paks.yaml +79 -0
  303. package/knowledge/baldursgate3/process.launch.yaml +34 -0
  304. package/knowledge/baldursgate3/progressions.yaml +92 -0
  305. package/knowledge/baldursgate3/publishing.modio-nexus.yaml +170 -0
  306. package/knowledge/baldursgate3/publishing.toolkit-modio.yaml +135 -0
  307. package/knowledge/baldursgate3/races.progression.yaml +228 -0
  308. package/knowledge/baldursgate3/scriptextender.console.yaml +257 -0
  309. package/knowledge/baldursgate3/scriptextender.yaml +252 -0
  310. package/knowledge/baldursgate3/stats.boosts.yaml +293 -0
  311. package/knowledge/baldursgate3/stats.equipment-kits.yaml +69 -0
  312. package/knowledge/baldursgate3/stats.interrupts.yaml +45 -0
  313. package/knowledge/baldursgate3/stats.itemcombos.yaml +65 -0
  314. package/knowledge/baldursgate3/stats.spells.yaml +293 -0
  315. package/knowledge/baldursgate3/stats.weapons.yaml +210 -0
  316. package/knowledge/baldursgate3/toolchain.divine.yaml +179 -0
  317. package/knowledge/baldursgate3/toolchain.lslib-formats.yaml +242 -0
  318. package/knowledge/baldursgate3/toolkit.levels.yaml +177 -0
  319. package/knowledge/baldursgate3/ui.icons-atlas.yaml +78 -0
  320. package/knowledge/baldursgate3/ui.icons.yaml +412 -0
  321. package/knowledge/baldursgate3/ui.mcm.yaml +161 -0
  322. package/knowledge/baldursgate3/vfx.materials.yaml +174 -0
  323. package/knowledge/baldursgate3/visuals.characterfix.yaml +73 -0
  324. package/knowledge/cyberpunk2077/animations.anims.yaml +118 -0
  325. package/knowledge/cyberpunk2077/anims.archivexl.yaml +163 -0
  326. package/knowledge/cyberpunk2077/anims.authoring.yaml +579 -0
  327. package/knowledge/cyberpunk2077/anims.encoder.yaml +156 -0
  328. package/knowledge/cyberpunk2077/anims.graph.yaml +629 -0
  329. package/knowledge/cyberpunk2077/anims.method.yaml +360 -0
  330. package/knowledge/cyberpunk2077/anims.pipeline.yaml +352 -0
  331. package/knowledge/cyberpunk2077/anims.roundtrip.yaml +222 -0
  332. package/knowledge/cyberpunk2077/anims.sets.yaml +176 -0
  333. package/knowledge/cyberpunk2077/archivexl.bodytypes.yaml +149 -0
  334. package/knowledge/cyberpunk2077/archivexl.dynamic-appearances.yaml +218 -0
  335. package/knowledge/cyberpunk2077/archivexl.manifest.yaml +214 -0
  336. package/knowledge/cyberpunk2077/audio.events.yaml +168 -0
  337. package/knowledge/cyberpunk2077/audio.sounds.yaml +164 -0
  338. package/knowledge/cyberpunk2077/audio.voicesets.yaml +146 -0
  339. package/knowledge/cyberpunk2077/cet.sandbox.yaml +219 -0
  340. package/knowledge/cyberpunk2077/clothing.garments.yaml +283 -0
  341. package/knowledge/cyberpunk2077/clothing.refits.yaml +215 -0
  342. package/knowledge/cyberpunk2077/codeware.overview.yaml +111 -0
  343. package/knowledge/cyberpunk2077/codeware.systems.yaml +192 -0
  344. package/knowledge/cyberpunk2077/drones.combat.yaml +416 -0
  345. package/knowledge/cyberpunk2077/drones.control.yaml +552 -0
  346. package/knowledge/cyberpunk2077/entities.appearance.yaml +166 -0
  347. package/knowledge/cyberpunk2077/equipmentex.slots.yaml +151 -0
  348. package/knowledge/cyberpunk2077/fx.effects.yaml +246 -0
  349. package/knowledge/cyberpunk2077/fx.lights.yaml +171 -0
  350. package/knowledge/cyberpunk2077/hair.modding.yaml +145 -0
  351. package/knowledge/cyberpunk2077/items.wiring.yaml +246 -0
  352. package/knowledge/cyberpunk2077/loadorder.archives.yaml +71 -0
  353. package/knowledge/cyberpunk2077/mesh.blender.yaml +209 -0
  354. package/knowledge/cyberpunk2077/nativedb.dump.yaml +98 -0
  355. package/knowledge/cyberpunk2077/npc.appearance.yaml +275 -0
  356. package/knowledge/cyberpunk2077/npc.behaviour.yaml +128 -0
  357. package/knowledge/cyberpunk2077/npc.locomotion.yaml +490 -0
  358. package/knowledge/cyberpunk2077/packaging.deploy.yaml +175 -0
  359. package/knowledge/cyberpunk2077/packaging.variants.yaml +176 -0
  360. package/knowledge/cyberpunk2077/player.control.yaml +918 -0
  361. package/knowledge/cyberpunk2077/process.assetbuild.yaml +154 -0
  362. package/knowledge/cyberpunk2077/process.lessons.yaml +276 -0
  363. package/knowledge/cyberpunk2077/quests.minor.yaml +46 -0
  364. package/knowledge/cyberpunk2077/quests.scenes.yaml +282 -0
  365. package/knowledge/cyberpunk2077/quickhacks.tiers.yaml +41 -0
  366. package/knowledge/cyberpunk2077/red4ext.api.yaml +645 -0
  367. package/knowledge/cyberpunk2077/red4ext.plugins.yaml +124 -0
  368. package/knowledge/cyberpunk2077/redhottools.hotreload.yaml +245 -0
  369. package/knowledge/cyberpunk2077/redscript.compile-log.yaml +76 -0
  370. package/knowledge/cyberpunk2077/redscript.modules.yaml +118 -0
  371. package/knowledge/cyberpunk2077/toolchain.wolvenkit.yaml +163 -0
  372. package/knowledge/cyberpunk2077/tweakdb.loot-recipes.yaml +184 -0
  373. package/knowledge/cyberpunk2077/tweakdb.melee.yaml +352 -0
  374. package/knowledge/cyberpunk2077/tweakdb.weapons.yaml +325 -0
  375. package/knowledge/cyberpunk2077/tweakxl.hotreload.yaml +151 -0
  376. package/knowledge/cyberpunk2077/tweakxl.yaml-syntax.yaml +208 -0
  377. package/knowledge/cyberpunk2077/ui.icons.yaml +259 -0
  378. package/knowledge/cyberpunk2077/ui.inkatlas.yaml +81 -0
  379. package/knowledge/cyberpunk2077/ui.modsettings.yaml +204 -0
  380. package/knowledge/cyberpunk2077/vehicles.control.yaml +475 -0
  381. package/knowledge/cyberpunk2077/vehicles.dashboard-ui.yaml +203 -0
  382. package/knowledge/cyberpunk2077/vehicles.entities.yaml +195 -0
  383. package/knowledge/cyberpunk2077/vehicles.wiring.yaml +137 -0
  384. package/knowledge/cyberpunk2077/wolvenkit.cli.yaml +167 -0
  385. package/knowledge/cyberpunk2077/workspot.system.yaml +55 -0
  386. package/knowledge/cyberpunk2077/world.interactions.yaml +41 -0
  387. package/knowledge/cyberpunk2077/world.loot-container.yaml +167 -0
  388. package/knowledge/cyberpunk2077/world.sectors.yaml +233 -0
  389. package/knowledge/eldenring/formats.binders.yaml +237 -0
  390. package/knowledge/eldenring/launcher.me3.yaml +230 -0
  391. package/knowledge/eldenring/launcher.modengine2.yaml +119 -0
  392. package/knowledge/eldenring/params.paramdef.yaml +149 -0
  393. package/knowledge/eldenring/params.regulation.yaml +142 -0
  394. package/knowledge/eldenring/saves.online.yaml +118 -0
  395. package/knowledge/eldenring/toolchain.cli.yaml +256 -0
  396. package/knowledge/nivalisnights/modding.runtime.yaml +157 -0
  397. package/knowledge/skyrimse/archives.bsa.yaml +212 -0
  398. package/knowledge/skyrimse/install.detection.yaml +41 -0
  399. package/knowledge/skyrimse/loadorder.files.yaml +242 -0
  400. package/knowledge/skyrimse/plugins.esl.yaml +189 -0
  401. package/knowledge/skyrimse/plugins.format.yaml +224 -0
  402. package/knowledge/skyrimse/skse.runtime.yaml +283 -0
  403. package/knowledge/skyrimse/toolchain.xedit-spriggit.yaml +336 -0
  404. package/knowledge/stardewvalley/contentpatcher.format.yaml +338 -0
  405. package/knowledge/stardewvalley/distribution.channels.yaml +236 -0
  406. package/knowledge/stardewvalley/game.versions.yaml +202 -0
  407. package/knowledge/stardewvalley/install.layout.yaml +277 -0
  408. package/knowledge/stardewvalley/smapi.loader.yaml +284 -0
  409. package/knowledge/stardewvalley/smapi.logs.yaml +329 -0
  410. package/knowledge/stardewvalley/smapi.manifest.yaml +293 -0
  411. package/knowledge/stardewvalley/toolchain.build.yaml +261 -0
  412. package/knowledge/stardewvalley/wiki.dataformat.yaml +130 -0
  413. package/knowledge/subnautica2/game.build.yaml +331 -0
  414. package/knowledge/subnautica2/modding.state.yaml +210 -0
  415. package/knowledge/subnautica2/ue5.containers.yaml +72 -0
  416. package/knowledge/subnautica2/ue5.paks.yaml +203 -0
  417. package/knowledge/subnautica2/ue5.tooling.yaml +175 -0
  418. package/knowledge/subnautica2/ue5.ue4ss.yaml +203 -0
  419. package/knowledge/valheim/bepinex.loader.yaml +247 -0
  420. package/knowledge/valheim/bepinex.plugins.yaml +251 -0
  421. package/knowledge/valheim/game.versions.yaml +321 -0
  422. package/knowledge/valheim/install.layout.yaml +216 -0
  423. package/knowledge/valheim/jotunn.library.yaml +334 -0
  424. package/knowledge/valheim/thunderstore.packaging.yaml +217 -0
  425. package/knowledge/valheim/toolchain.build.yaml +111 -0
  426. package/package.json +29 -0
  427. package/scaffolds/baldursgate3/new-class-injection.yaml +148 -0
  428. package/scaffolds/baldursgate3/new-item.yaml +264 -0
  429. package/scaffolds/baldursgate3/new-mod.yaml +174 -0
  430. package/scaffolds/baldursgate3/new-passive.yaml +100 -0
  431. package/scaffolds/baldursgate3/new-project.yaml +118 -0
  432. package/scaffolds/baldursgate3/new-quest-stub.yaml +159 -0
  433. package/scaffolds/baldursgate3/new-spell.yaml +102 -0
  434. package/scaffolds/baldursgate3/new-status.yaml +119 -0
  435. package/scaffolds/baldursgate3/sequences/subclass-kit.yaml +50 -0
  436. package/scaffolds/cyberpunk2077/edit-tweak.yaml +70 -0
  437. package/scaffolds/cyberpunk2077/mesh-export-preset.yaml +170 -0
  438. package/scaffolds/cyberpunk2077/new-entity-patch.yaml +255 -0
  439. package/scaffolds/cyberpunk2077/new-garment-refit.yaml +279 -0
  440. package/scaffolds/cyberpunk2077/new-item-chain.yaml +548 -0
  441. package/scaffolds/cyberpunk2077/new-localization.yaml +130 -0
  442. package/scaffolds/cyberpunk2077/new-mod-settings.yaml +133 -0
  443. package/scaffolds/cyberpunk2077/new-player-replacer.yaml +1003 -0
  444. package/scaffolds/cyberpunk2077/new-project.yaml +157 -0
  445. package/scaffolds/cyberpunk2077/new-sector-node.yaml +156 -0
  446. package/scaffolds/cyberpunk2077/new-tweak.yaml +121 -0
  447. package/scaffolds/cyberpunk2077/new-vehicle-livery.yaml +148 -0
  448. package/scaffolds/cyberpunk2077/new-workspot-entity.yaml +177 -0
  449. package/scaffolds/cyberpunk2077/sequences/iconic-weapon.yaml +72 -0
  450. package/scaffolds/cyberpunk2077/sequences/quickhack-takeover.yaml +213 -0
  451. package/scaffolds/stardewvalley/new-cp-patch.yaml +106 -0
  452. package/scaffolds/stardewvalley/new-csharp-mod.yaml +195 -0
  453. package/scaffolds/stardewvalley/new-project.yaml +184 -0
  454. package/scaffolds/stardewvalley/sequences/content-pack.yaml +31 -0
  455. package/scripts/mw-tool.mjs +184 -0
@@ -0,0 +1,72 @@
1
+ import { readFileSync } from "node:fs";
2
+ import * as path from "node:path";
3
+ import { findProjectFile, loadProject } from "./project/index.js";
4
+ import { claudeAssetsRoot, interpolateTemplate, modwrightVersion } from "./scaffold/claude-assets.js";
5
+ export const SKILL_PROMPTS = [
6
+ { name: "mw-validate", templatePath: "skills/mw-validate.md" },
7
+ { name: "mw-scaffold", templatePath: "skills/mw-scaffold.md" },
8
+ { name: "mw-new-spell", templatePath: "skills/mw-new-spell.md", games: ["baldursgate3"] },
9
+ { name: "mw-new-item", templatePath: "skills/mw-new-item.md", games: ["cyberpunk2077"] },
10
+ { name: "mw-verify", templatePath: "skills/mw-verify.md" },
11
+ { name: "mw-ship", templatePath: "skills/mw-ship.md" },
12
+ { name: "mw-author", templatePath: "skills/mw-author.md" },
13
+ ];
14
+ export function parseSkillTemplate(content) {
15
+ const text = content.replace(/\r\n/g, "\n");
16
+ const frontmatter = {};
17
+ let body = text;
18
+ const match = /^---\n([\s\S]*?)\n---\n/.exec(text);
19
+ if (match) {
20
+ for (const line of match[1].split("\n")) {
21
+ const colon = line.indexOf(":");
22
+ if (colon <= 0)
23
+ continue;
24
+ frontmatter[line.slice(0, colon).trim()] = line.slice(colon + 1).trim();
25
+ }
26
+ body = text.slice(match[0].length);
27
+ }
28
+ body = body.replace(/^<!--\s*modwright:asset[^>]*-->\n?/m, "").replace(/^\n+/, "");
29
+ return { frontmatter, body };
30
+ }
31
+ export function loadSkillPromptCatalog() {
32
+ return SKILL_PROMPTS.map((spec) => {
33
+ const file = path.join(claudeAssetsRoot(), spec.templatePath);
34
+ const template = parseSkillTemplate(readFileSync(file, "utf8"));
35
+ const hint = template.frontmatter["argument-hint"];
36
+ const scope = spec.games ? ` (${spec.games.join(", ")} only)` : "";
37
+ const description = `${template.frontmatter.description ?? spec.name}${scope}${hint ? ` Arguments: ${hint}.` : ""}`;
38
+ return { ...spec, title: spec.name, description, template };
39
+ });
40
+ }
41
+ export async function renderSkillPrompt(entry, start) {
42
+ const from = start ?? process.cwd();
43
+ const version = modwrightVersion();
44
+ try {
45
+ const file = from.toLowerCase().endsWith(".json") ? from : await findProjectFile(from);
46
+ if (!file)
47
+ throw new Error(`no modwright.json at or above ${from}`);
48
+ const resolved = await loadProject(file);
49
+ const { name, game } = resolved.project;
50
+ if (entry.games && !entry.games.includes(game)) {
51
+ return {
52
+ name: entry.name,
53
+ text: `${entry.name} is a ${entry.games.join("/")} skill; ${name} is a ${game} project. Use \`scaffold\` with \`action: "list"\` and \`game: "${game}"\` to see what applies here.`,
54
+ project: { name, game, file },
55
+ note: "game mismatch",
56
+ };
57
+ }
58
+ return {
59
+ name: entry.name,
60
+ text: interpolateTemplate(entry.template.body, { version, game, project: name }),
61
+ project: { name, game, file },
62
+ };
63
+ }
64
+ catch (error) {
65
+ const note = `No project was loaded (${error.message}); \`<project>\` and \`<game>\` below are placeholders — call \`project_info\` first and read them from its result.`;
66
+ return {
67
+ name: entry.name,
68
+ text: `${note}\n\n${interpolateTemplate(entry.template.body, { version, game: "<game>", project: "<project>" })}`,
69
+ note,
70
+ };
71
+ }
72
+ }
@@ -0,0 +1,35 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ const execFileAsync = promisify(execFile);
4
+ const defaultRunner = async (args) => {
5
+ const { stdout } = await execFileAsync("reg", args, { timeout: 10_000, windowsHide: true });
6
+ return stdout;
7
+ };
8
+ export function parseRegQueryValue(stdout, valueName) {
9
+ const wanted = valueName.toLowerCase();
10
+ for (const raw of stdout.split(/\r?\n/)) {
11
+ const line = raw.trim();
12
+ const m = /^(?<name>.+?)\s{2,}(?<type>REG_[A-Z_]+)\s{2,}(?<data>.*)$/.exec(line) ?? /^(?<name>.+?)\t+(?<type>REG_[A-Z_]+)\t+(?<data>.*)$/.exec(line);
13
+ if (!m?.groups)
14
+ continue;
15
+ if (m.groups.name.trim().toLowerCase() !== wanted)
16
+ continue;
17
+ return m.groups.data.trim();
18
+ }
19
+ return undefined;
20
+ }
21
+ export async function readRegistryValue(keys, valueName, runner = defaultRunner) {
22
+ if (process.platform !== "win32" && runner === defaultRunner)
23
+ return undefined;
24
+ for (const key of keys) {
25
+ try {
26
+ const stdout = await runner(["query", key, "/v", valueName]);
27
+ const value = parseRegQueryValue(stdout, valueName);
28
+ if (value !== undefined && value.length > 0)
29
+ return { key, value };
30
+ }
31
+ catch {
32
+ }
33
+ }
34
+ return undefined;
35
+ }
@@ -0,0 +1,206 @@
1
+ import { createReadStream, promises as fs } from "node:fs";
2
+ import * as crypto from "node:crypto";
3
+ import * as path from "node:path";
4
+ import { z } from "zod";
5
+ import { isDirectory, readLinkTarget } from "../fsutil.js";
6
+ export const backupManifestFileEntrySchema = z
7
+ .object({
8
+ originalPath: z.string().min(1),
9
+ backupPath: z.string().min(1),
10
+ sizeBytes: z.number().int().nonnegative(),
11
+ sha256: z.string().length(64),
12
+ })
13
+ .strict();
14
+ export const backupManifestLinkEntrySchema = z
15
+ .object({
16
+ originalPath: z.string().min(1),
17
+ kind: z.literal("link"),
18
+ linkTarget: z.string().min(1),
19
+ })
20
+ .strict();
21
+ export const backupManifestEntrySchema = z.union([
22
+ backupManifestFileEntrySchema,
23
+ backupManifestLinkEntrySchema,
24
+ ]);
25
+ export const backupManifestSchema = z
26
+ .object({
27
+ purpose: z.string(),
28
+ time: z.string(),
29
+ entries: z.array(backupManifestEntrySchema),
30
+ })
31
+ .strict();
32
+ export const MANIFEST_FILE = "manifest.json";
33
+ export function sanitizeForBackup(absPath) {
34
+ let p = absPath.replace(/\\/g, "/");
35
+ const drive = /^([A-Za-z]):\//.exec(p);
36
+ if (drive) {
37
+ p = p.slice(drive[0].length);
38
+ return [drive[1], ...p.split("/").filter(Boolean)];
39
+ }
40
+ const unc = /^\/\/([^/]+)\/(.*)$/.exec(p);
41
+ if (unc) {
42
+ return ["unc", unc[1], ...unc[2].split("/").filter(Boolean)];
43
+ }
44
+ return p.split("/").filter(Boolean);
45
+ }
46
+ export function backupDestFor(backupDir, originalAbsPath) {
47
+ return path.join(backupDir, "data", ...sanitizeForBackup(originalAbsPath));
48
+ }
49
+ export async function sha256File(p) {
50
+ const hash = crypto.createHash("sha256");
51
+ let sizeBytes = 0;
52
+ for await (const chunk of createReadStream(p)) {
53
+ sizeBytes += chunk.length;
54
+ hash.update(chunk);
55
+ }
56
+ return { sha256: hash.digest("hex"), sizeBytes };
57
+ }
58
+ export function tempSiblingPath(target) {
59
+ const dir = path.dirname(target);
60
+ const base = path.basename(target);
61
+ return path.join(dir, `.${base}.mwtmp-${crypto.randomBytes(6).toString("hex")}`);
62
+ }
63
+ export async function writeFileAtomic(target, content) {
64
+ await fs.mkdir(path.dirname(target), { recursive: true });
65
+ const tmp = tempSiblingPath(target);
66
+ try {
67
+ await fs.writeFile(tmp, content);
68
+ await fs.rename(tmp, target);
69
+ }
70
+ catch (err) {
71
+ await fs.rm(tmp, { force: true }).catch(() => { });
72
+ throw err;
73
+ }
74
+ }
75
+ export async function copyFilePreservingMtime(src, dest) {
76
+ await fs.mkdir(path.dirname(dest), { recursive: true });
77
+ const tmp = tempSiblingPath(dest);
78
+ try {
79
+ await fs.copyFile(src, tmp);
80
+ const stat = await fs.stat(src);
81
+ await fs.utimes(tmp, stat.atime, stat.mtime).catch(() => {
82
+ });
83
+ await fs.rename(tmp, dest);
84
+ }
85
+ catch (err) {
86
+ await fs.rm(tmp, { force: true }).catch(() => { });
87
+ throw err;
88
+ }
89
+ }
90
+ export async function copyTreePreservingMtime(src, dest) {
91
+ if (await isDirectory(src)) {
92
+ await fs.mkdir(dest, { recursive: true });
93
+ const entries = await fs.readdir(src, { withFileTypes: true });
94
+ for (const entry of entries) {
95
+ await copyTreePreservingMtime(path.join(src, entry.name), path.join(dest, entry.name));
96
+ }
97
+ }
98
+ else {
99
+ await copyFilePreservingMtime(src, dest);
100
+ }
101
+ }
102
+ export class ManifestWriter {
103
+ backupDir;
104
+ purpose;
105
+ entries = [];
106
+ constructor(backupDir, purpose) {
107
+ this.backupDir = backupDir;
108
+ this.purpose = purpose;
109
+ }
110
+ get hasEntries() {
111
+ return this.entries.length > 0;
112
+ }
113
+ async addFile(originalAbsPath) {
114
+ const dest = backupDestFor(this.backupDir, originalAbsPath);
115
+ await copyFilePreservingMtime(originalAbsPath, dest);
116
+ const { sha256, sizeBytes } = await sha256File(originalAbsPath);
117
+ const backupPath = path.relative(this.backupDir, dest).split(path.sep).join("/");
118
+ this.entries.push({ originalPath: originalAbsPath, backupPath, sizeBytes, sha256 });
119
+ }
120
+ async addLink(originalAbsPath) {
121
+ const linkTarget = await readLinkTarget(originalAbsPath);
122
+ this.entries.push({ originalPath: originalAbsPath, kind: "link", linkTarget });
123
+ return this.backupDir;
124
+ }
125
+ async addTree(originalAbsDir) {
126
+ const entries = await fs.readdir(originalAbsDir, { withFileTypes: true });
127
+ for (const entry of entries) {
128
+ const child = path.join(originalAbsDir, entry.name);
129
+ if (entry.isSymbolicLink()) {
130
+ await this.addLink(child);
131
+ }
132
+ else if (entry.isDirectory()) {
133
+ await this.addTree(child);
134
+ }
135
+ else if (entry.isFile()) {
136
+ await this.addFile(child);
137
+ }
138
+ }
139
+ }
140
+ async addPath(originalAbsPath) {
141
+ const before = this.entries.length;
142
+ let lst;
143
+ try {
144
+ lst = await fs.lstat(originalAbsPath);
145
+ }
146
+ catch {
147
+ return undefined;
148
+ }
149
+ if (lst.isSymbolicLink()) {
150
+ return this.addLink(originalAbsPath);
151
+ }
152
+ if (lst.isDirectory()) {
153
+ await this.addTree(originalAbsPath);
154
+ }
155
+ else {
156
+ await this.addFile(originalAbsPath);
157
+ }
158
+ return this.entries.length > before ? backupDestFor(this.backupDir, originalAbsPath) : undefined;
159
+ }
160
+ async flush(now) {
161
+ const manifest = { purpose: this.purpose, time: now.toISOString(), entries: this.entries };
162
+ await fs.writeFile(path.join(this.backupDir, MANIFEST_FILE), JSON.stringify(manifest, null, 2), "utf8");
163
+ }
164
+ }
165
+ export async function readManifest(backupDir) {
166
+ const manifestPath = path.join(backupDir, MANIFEST_FILE);
167
+ let raw;
168
+ try {
169
+ raw = await fs.readFile(manifestPath, "utf8");
170
+ }
171
+ catch (err) {
172
+ throw new Error(`safety: no ${MANIFEST_FILE} found at ${backupDir}: ${err.message}`);
173
+ }
174
+ let json;
175
+ try {
176
+ json = JSON.parse(raw);
177
+ }
178
+ catch (err) {
179
+ throw new Error(`safety: ${manifestPath} is not valid JSON: ${err.message}`);
180
+ }
181
+ const result = backupManifestSchema.safeParse(json);
182
+ if (!result.success) {
183
+ const lines = result.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`);
184
+ throw new Error(`safety: ${manifestPath} is invalid:\n${lines.join("\n")}`);
185
+ }
186
+ return result.data;
187
+ }
188
+ export function backupTimestamp(now) {
189
+ const pad = (n, width = 2) => String(n).padStart(width, "0");
190
+ return (`${now.getUTCFullYear()}${pad(now.getUTCMonth() + 1)}${pad(now.getUTCDate())}` +
191
+ `-${pad(now.getUTCHours())}${pad(now.getUTCMinutes())}${pad(now.getUTCSeconds())}`);
192
+ }
193
+ export async function uniqueBackupDir(backupRoot, timestamp) {
194
+ await fs.mkdir(backupRoot, { recursive: true });
195
+ for (let n = 1;; n += 1) {
196
+ const candidate = n === 1 ? path.join(backupRoot, timestamp) : path.join(backupRoot, `${timestamp}-${n}`);
197
+ try {
198
+ await fs.mkdir(candidate);
199
+ return candidate;
200
+ }
201
+ catch (err) {
202
+ if (err.code !== "EEXIST")
203
+ throw err;
204
+ }
205
+ }
206
+ }
@@ -0,0 +1,188 @@
1
+ import { promises as fs } from "node:fs";
2
+ import * as path from "node:path";
3
+ import { pathExists } from "../fsutil.js";
4
+ import { userDataDir } from "../home.js";
5
+ import { normalizePlan } from "./plan.js";
6
+ import { isLockError } from "./restore.js";
7
+ import { ManifestWriter, backupTimestamp, copyTreePreservingMtime, uniqueBackupDir, writeFileAtomic, } from "./backup.js";
8
+ function errorMessage(err) {
9
+ return err instanceof Error ? err.message : String(err);
10
+ }
11
+ async function lstatExists(p) {
12
+ try {
13
+ await fs.lstat(p);
14
+ return true;
15
+ }
16
+ catch {
17
+ return false;
18
+ }
19
+ }
20
+ async function applyOp(nop, ensureBackup) {
21
+ const op = nop.op;
22
+ switch (op.kind) {
23
+ case "mkdir": {
24
+ if (nop.skipEligible)
25
+ return { op, status: "skipped" };
26
+ await fs.mkdir(op.path, { recursive: true });
27
+ return { op, status: "applied" };
28
+ }
29
+ case "create":
30
+ case "overwrite": {
31
+ if (nop.skipEligible)
32
+ return { op, status: "skipped" };
33
+ let backedUpTo;
34
+ if (op.kind === "overwrite") {
35
+ backedUpTo = await (await ensureBackup()).addPath(op.path);
36
+ }
37
+ await writeFileAtomic(op.path, op.content);
38
+ return { op, status: "applied", backedUpTo };
39
+ }
40
+ case "copy": {
41
+ if (nop.skipEligible)
42
+ return { op, status: "skipped" };
43
+ let backedUpTo;
44
+ if (await pathExists(op.to)) {
45
+ backedUpTo = await (await ensureBackup()).addPath(op.to);
46
+ }
47
+ await copyTreePreservingMtime(op.from, op.to);
48
+ return { op, status: "applied", backedUpTo };
49
+ }
50
+ case "delete": {
51
+ if (nop.skipEligible)
52
+ return { op, status: "skipped" };
53
+ const backedUpTo = await (await ensureBackup()).addPath(op.path);
54
+ await fs.rm(op.path, { recursive: true, force: true });
55
+ return { op, status: "applied", backedUpTo };
56
+ }
57
+ case "unlink": {
58
+ if (nop.skipEligible)
59
+ return { op, status: "skipped" };
60
+ const backedUpTo = await (await ensureBackup()).addLink(op.path);
61
+ await fs.rm(op.path, { recursive: true, force: true });
62
+ return { op, status: "applied", backedUpTo };
63
+ }
64
+ case "junction": {
65
+ let backedUpTo;
66
+ if (await lstatExists(op.path)) {
67
+ backedUpTo = await (await ensureBackup()).addPath(op.path);
68
+ await fs.rm(op.path, { recursive: true, force: true });
69
+ }
70
+ await fs.mkdir(path.dirname(op.path), { recursive: true });
71
+ const linkType = process.platform === "win32" ? "junction" : "symlink";
72
+ await fs.symlink(op.target, op.path, process.platform === "win32" ? "junction" : undefined);
73
+ const result = { op, status: "applied", backedUpTo, linkType };
74
+ return result;
75
+ }
76
+ }
77
+ }
78
+ async function applyWithRetry(nop, ensureBackup, retry) {
79
+ const done = new Map();
80
+ const once = (kind, p, run) => {
81
+ const key = `${kind}:${p}`;
82
+ let pending = done.get(key);
83
+ if (!pending) {
84
+ pending = run();
85
+ done.set(key, pending);
86
+ pending.catch(() => done.delete(key));
87
+ }
88
+ return pending;
89
+ };
90
+ const backupOnce = async () => {
91
+ const writer = await ensureBackup();
92
+ return {
93
+ addPath: (p) => once("path", p, () => writer.addPath(p)),
94
+ addLink: (p) => once("link", p, () => writer.addLink(p)),
95
+ };
96
+ };
97
+ for (let attempt = 0;; attempt++) {
98
+ try {
99
+ return await applyOp(nop, backupOnce);
100
+ }
101
+ catch (err) {
102
+ const delay = retry?.shouldRetry(nop.op, err) ? retry.delaysMs[attempt] : undefined;
103
+ if (delay === undefined)
104
+ throw err;
105
+ retry.onRetry?.(nop.op, attempt + 1, err);
106
+ await (retry.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))))(delay);
107
+ }
108
+ }
109
+ }
110
+ export async function executePlan(plan, options) {
111
+ const mode = options.mode ?? "dry-run";
112
+ const now = options.now ?? (() => new Date());
113
+ const normalized = await normalizePlan(plan);
114
+ if (mode === "dry-run") {
115
+ const operations = normalized.operations.map((nop) => ({
116
+ op: nop.op,
117
+ status: "planned",
118
+ }));
119
+ return {
120
+ mode,
121
+ purpose: plan.purpose,
122
+ applied: false,
123
+ operations,
124
+ confidence: options.confidence,
125
+ next: 'Nothing was changed. Re-run with { mode: "apply" } to write these changes; any overwrite or delete is backed up first.',
126
+ };
127
+ }
128
+ const backupRoot = plan.backupRoot ?? path.join(userDataDir(), "backups");
129
+ const timestamp = backupTimestamp(now());
130
+ let manifestWriter;
131
+ let backupDir;
132
+ const ensureBackup = async () => {
133
+ if (!manifestWriter) {
134
+ backupDir = await uniqueBackupDir(backupRoot, timestamp);
135
+ manifestWriter = new ManifestWriter(backupDir, plan.purpose);
136
+ }
137
+ return manifestWriter;
138
+ };
139
+ const results = [];
140
+ let stopped = false;
141
+ for (const nop of normalized.operations) {
142
+ if (stopped) {
143
+ results.push({ op: nop.op, status: "skipped" });
144
+ continue;
145
+ }
146
+ try {
147
+ results.push(await applyWithRetry(nop, ensureBackup, options.retry));
148
+ }
149
+ catch (err) {
150
+ const op = nop.op;
151
+ const deferrable = (op.kind === "delete" || op.kind === "overwrite") && options.deferOnLock?.(op) && isLockError(err);
152
+ if (deferrable) {
153
+ results.push({ op, status: "deferred", error: errorMessage(err) });
154
+ continue;
155
+ }
156
+ stopped = true;
157
+ results.push({ op, status: "failed", error: errorMessage(err) });
158
+ }
159
+ }
160
+ if (manifestWriter?.hasEntries) {
161
+ await manifestWriter.flush(now());
162
+ }
163
+ else if (backupDir) {
164
+ await fs.rm(backupDir, { recursive: true, force: true }).catch(() => { });
165
+ backupDir = undefined;
166
+ }
167
+ const deferredPaths = results.flatMap((r) => r.status === "deferred" && (r.op.kind === "delete" || r.op.kind === "overwrite") ? [r.op.path] : []);
168
+ const applied = results.every((r) => r.status === "applied" || r.status === "skipped" || r.status === "deferred");
169
+ const next = !applied
170
+ ? backupDir
171
+ ? `An operation failed; later operations were skipped rather than run against a partially-applied plan. ` +
172
+ `To undo what did apply, call restoreBackup("${backupDir}", { mode: "apply" }), then fix the problem and retry.`
173
+ : "An operation failed before anything needed backing up; later operations were skipped. Fix the problem and retry."
174
+ : deferredPaths.length > 0
175
+ ? `${deferredPaths.length} operation(s) could not be written because something holds them open (the game, if it is running): ` +
176
+ `${deferredPaths.join(", ")}. ` +
177
+ `Everything else in the plan still ran; re-run once the holder is closed to retry them.`
178
+ : undefined;
179
+ return {
180
+ mode,
181
+ purpose: plan.purpose,
182
+ applied,
183
+ operations: results,
184
+ backupDir,
185
+ confidence: options.confidence,
186
+ next,
187
+ };
188
+ }
@@ -0,0 +1,5 @@
1
+ export * from "./types.js";
2
+ export { planFile, planMkdir, planCopy, planDelete, planJunction, planUnlink, normalizePlan, isIdenticalFile } from "./plan.js";
3
+ export { executePlan } from "./execute.js";
4
+ export { restoreBackup, listBackups, isLockError } from "./restore.js";
5
+ export { MANIFEST_FILE, backupManifestSchema, backupManifestEntrySchema, backupManifestFileEntrySchema, backupManifestLinkEntrySchema, readManifest, sha256File, } from "./backup.js";
@@ -0,0 +1,122 @@
1
+ import { promises as fs } from "node:fs";
2
+ import * as path from "node:path";
3
+ import { isDirectory, pathExists } from "../fsutil.js";
4
+ import { sha256File } from "./backup.js";
5
+ export function planFile(absPath, content) {
6
+ return { kind: "create", path: absPath, content };
7
+ }
8
+ export function planMkdir(absPath) {
9
+ return { kind: "mkdir", path: absPath };
10
+ }
11
+ export function planCopy(fromAbsPath, toAbsPath) {
12
+ return { kind: "copy", from: fromAbsPath, to: toAbsPath };
13
+ }
14
+ export function planDelete(absPath) {
15
+ return { kind: "delete", path: absPath };
16
+ }
17
+ export function planJunction(absPath, targetAbsPath) {
18
+ return { kind: "junction", path: absPath, target: targetAbsPath };
19
+ }
20
+ export function planUnlink(absPath) {
21
+ return { kind: "unlink", path: absPath };
22
+ }
23
+ function opPaths(op) {
24
+ switch (op.kind) {
25
+ case "copy":
26
+ return [op.from, op.to];
27
+ case "junction":
28
+ return [op.path, op.target];
29
+ default:
30
+ return [op.path];
31
+ }
32
+ }
33
+ function assertAbsolute(label, p) {
34
+ if (!path.isAbsolute(p)) {
35
+ throw new Error(`safety: ${label} must be an absolute path, got "${p}"`);
36
+ }
37
+ }
38
+ async function hasIdenticalContent(filePath, content) {
39
+ const incoming = typeof content === "string" ? Buffer.from(content, "utf8") : Buffer.from(content);
40
+ let existing;
41
+ try {
42
+ const stat = await fs.stat(filePath);
43
+ if (!stat.isFile() || stat.size !== incoming.length)
44
+ return false;
45
+ existing = await fs.readFile(filePath);
46
+ }
47
+ catch {
48
+ return false;
49
+ }
50
+ return existing.equals(incoming);
51
+ }
52
+ export async function isIdenticalFile(from, to) {
53
+ try {
54
+ const [a, b] = await Promise.all([fs.stat(from), fs.lstat(to)]);
55
+ if (!a.isFile() || !b.isFile() || a.size !== b.size)
56
+ return false;
57
+ const [ha, hb] = await Promise.all([sha256File(from), sha256File(to)]);
58
+ return ha.sha256 === hb.sha256;
59
+ }
60
+ catch {
61
+ return false;
62
+ }
63
+ }
64
+ async function normalizeOp(op) {
65
+ for (const p of opPaths(op))
66
+ assertAbsolute(`${op.kind} path`, p);
67
+ switch (op.kind) {
68
+ case "create":
69
+ case "overwrite": {
70
+ const exists = await pathExists(op.path);
71
+ if (!exists) {
72
+ return { op: { ...op, kind: "create" }, skipEligible: false };
73
+ }
74
+ const identical = await hasIdenticalContent(op.path, op.content);
75
+ return {
76
+ op: { ...op, kind: "overwrite" },
77
+ skipEligible: identical,
78
+ skipReason: identical ? "target already has identical content" : undefined,
79
+ };
80
+ }
81
+ case "mkdir": {
82
+ const exists = await isDirectory(op.path);
83
+ return { op, skipEligible: exists, skipReason: exists ? "directory already exists" : undefined };
84
+ }
85
+ case "delete": {
86
+ const exists = await pathExists(op.path);
87
+ return { op, skipEligible: !exists, skipReason: exists ? undefined : "path does not exist" };
88
+ }
89
+ case "unlink": {
90
+ let lst;
91
+ try {
92
+ lst = await fs.lstat(op.path);
93
+ }
94
+ catch {
95
+ return { op, skipEligible: true, skipReason: "path does not exist" };
96
+ }
97
+ if (!lst.isSymbolicLink()) {
98
+ return {
99
+ op,
100
+ skipEligible: true,
101
+ skipReason: "path is not a symbolic link or junction; unlink refuses to delete a real file or directory",
102
+ };
103
+ }
104
+ return { op, skipEligible: false };
105
+ }
106
+ case "copy": {
107
+ const identical = await isIdenticalFile(op.from, op.to);
108
+ return { op, skipEligible: identical, skipReason: identical ? "target already has identical content" : undefined };
109
+ }
110
+ case "junction":
111
+ return { op, skipEligible: false };
112
+ }
113
+ }
114
+ export async function normalizePlan(plan) {
115
+ if (plan.backupRoot !== undefined)
116
+ assertAbsolute("backupRoot", plan.backupRoot);
117
+ const operations = [];
118
+ for (const op of plan.operations) {
119
+ operations.push(await normalizeOp(op));
120
+ }
121
+ return { purpose: plan.purpose, backupRoot: plan.backupRoot, operations };
122
+ }