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.
- package/LICENSE +35 -0
- package/README.md +212 -0
- package/assets/claude/agents/mw-explore.md +46 -0
- package/assets/claude/agents/mw-fixture.md +33 -0
- package/assets/claude/agents/mw-rule-author.md +51 -0
- package/assets/claude/agents/mw-scaffold-author.md +48 -0
- package/assets/claude/skills/mw-author.md +59 -0
- package/assets/claude/skills/mw-new-item.md +51 -0
- package/assets/claude/skills/mw-new-spell.md +50 -0
- package/assets/claude/skills/mw-scaffold.md +59 -0
- package/assets/claude/skills/mw-ship.md +57 -0
- package/assets/claude/skills/mw-validate.md +51 -0
- package/assets/claude/skills/mw-verify.md +59 -0
- package/assets/nivalisnights/extract_resources.py +177 -0
- package/bridges/bg3-se/Mods/ModWrightBridge/ScriptExtender/Config.json +5 -0
- package/bridges/bg3-se/Mods/ModWrightBridge/ScriptExtender/Lua/BootstrapClient.lua +55 -0
- package/bridges/bg3-se/Mods/ModWrightBridge/ScriptExtender/Lua/BootstrapServer.lua +81 -0
- package/bridges/bg3-se/Mods/ModWrightBridge/ScriptExtender/Lua/ModWrightBridge/Log.lua +96 -0
- package/bridges/bg3-se/Mods/ModWrightBridge/ScriptExtender/Lua/ModWrightBridge/Probes.lua +1038 -0
- package/bridges/bg3-se/Mods/ModWrightBridge/ScriptExtender/Lua/ModWrightBridge/ProbesClient.lua +74 -0
- package/bridges/bg3-se/Mods/ModWrightBridge/ScriptExtender/Lua/ModWrightBridge/Protocol.lua +985 -0
- package/bridges/bg3-se/Mods/ModWrightBridge/ScriptExtender/Lua/ModWrightBridge/Watch.lua +175 -0
- package/bridges/bg3-se/Mods/ModWrightBridge/meta.lsx +45 -0
- package/bridges/bg3-se/README.md +99 -0
- package/bridges/bg3-se/bridge.json +68 -0
- package/bridges/cp2077-cet/ModWrightBridge/init.lua +375 -0
- package/bridges/cp2077-cet/ModWrightBridge/probes.lua +1104 -0
- package/bridges/cp2077-cet/ModWrightBridge/protocol.lua +1306 -0
- package/bridges/cp2077-cet/README.md +165 -0
- package/bridges/cp2077-cet/bridge.json +53 -0
- package/dist/core/bridge/arm.js +175 -0
- package/dist/core/bridge/cet-console.js +26 -0
- package/dist/core/bridge/cet-log.js +362 -0
- package/dist/core/bridge/index.js +11 -0
- package/dist/core/bridge/locate.js +35 -0
- package/dist/core/bridge/paths.js +34 -0
- package/dist/core/bridge/pending.js +89 -0
- package/dist/core/bridge/poll.js +155 -0
- package/dist/core/bridge/stage.js +100 -0
- package/dist/core/bridge/status.js +97 -0
- package/dist/core/bridge/types.js +145 -0
- package/dist/core/bridge/validate.js +107 -0
- package/dist/core/build/index.js +6 -0
- package/dist/core/build/manifest.js +47 -0
- package/dist/core/build/paths.js +32 -0
- package/dist/core/build/run.js +342 -0
- package/dist/core/build/stage.js +265 -0
- package/dist/core/build/steps/bg3.js +129 -0
- package/dist/core/build/steps/core.js +195 -0
- package/dist/core/build/steps/cyberpunk.js +95 -0
- package/dist/core/build/steps/listing.js +35 -0
- package/dist/core/build/types.js +13 -0
- package/dist/core/build/zip.js +148 -0
- package/dist/core/compat/check.js +113 -0
- package/dist/core/compat/pe.js +220 -0
- package/dist/core/compat/types.js +1 -0
- package/dist/core/compat/version.js +70 -0
- package/dist/core/deploy/guards.js +292 -0
- package/dist/core/deploy/index.js +5 -0
- package/dist/core/deploy/plan.js +696 -0
- package/dist/core/deploy/reload-apply.js +361 -0
- package/dist/core/deploy/reload.js +358 -0
- package/dist/core/deploy/sources.js +169 -0
- package/dist/core/deploy/types.js +1 -0
- package/dist/core/epic.js +54 -0
- package/dist/core/fsutil.js +191 -0
- package/dist/core/home.js +70 -0
- package/dist/core/knowledge/facts.js +229 -0
- package/dist/core/knowledge/index.js +4 -0
- package/dist/core/knowledge/search.js +112 -0
- package/dist/core/knowledge/store.js +123 -0
- package/dist/core/knowledge/types.js +1 -0
- package/dist/core/ledger/check.js +42 -0
- package/dist/core/ledger/hooks.js +80 -0
- package/dist/core/ledger/index.js +7 -0
- package/dist/core/ledger/query.js +17 -0
- package/dist/core/ledger/record.js +84 -0
- package/dist/core/ledger/status.js +53 -0
- package/dist/core/ledger/store.js +98 -0
- package/dist/core/ledger/types.js +108 -0
- package/dist/core/logs/index.js +4 -0
- package/dist/core/logs/registry.js +50 -0
- package/dist/core/logs/scan.js +195 -0
- package/dist/core/logs/triage.js +160 -0
- package/dist/core/logs/types.js +1 -0
- package/dist/core/lsx.js +10 -0
- package/dist/core/lz4.js +66 -0
- package/dist/core/mods.js +229 -0
- package/dist/core/process.js +49 -0
- package/dist/core/project/build-steps.js +83 -0
- package/dist/core/project/describe.js +74 -0
- package/dist/core/project/index.js +5 -0
- package/dist/core/project/load.js +146 -0
- package/dist/core/project/schema.js +264 -0
- package/dist/core/project/types.js +3 -0
- package/dist/core/prompts.js +72 -0
- package/dist/core/registry.js +35 -0
- package/dist/core/safety/backup.js +206 -0
- package/dist/core/safety/execute.js +188 -0
- package/dist/core/safety/index.js +5 -0
- package/dist/core/safety/plan.js +122 -0
- package/dist/core/safety/restore.js +172 -0
- package/dist/core/safety/types.js +1 -0
- package/dist/core/scaffold/bootstrap.js +7 -0
- package/dist/core/scaffold/catalog.js +28 -0
- package/dist/core/scaffold/claude-assets.js +271 -0
- package/dist/core/scaffold/collisions.js +64 -0
- package/dist/core/scaffold/emit.js +356 -0
- package/dist/core/scaffold/engine.js +680 -0
- package/dist/core/scaffold/ids.js +78 -0
- package/dist/core/scaffold/index.js +10 -0
- package/dist/core/scaffold/jsonpath.js +121 -0
- package/dist/core/scaffold/load.js +81 -0
- package/dist/core/scaffold/render.js +85 -0
- package/dist/core/scaffold/sequence.js +396 -0
- package/dist/core/scaffold/types.js +309 -0
- package/dist/core/steam.js +63 -0
- package/dist/core/surfaceutil.js +36 -0
- package/dist/core/testplan/check.js +67 -0
- package/dist/core/testplan/index.js +5 -0
- package/dist/core/testplan/load.js +100 -0
- package/dist/core/testplan/registry.js +317 -0
- package/dist/core/testplan/run.js +389 -0
- package/dist/core/testplan/types.js +82 -0
- package/dist/core/text/converters/bg3.js +50 -0
- package/dist/core/text/converters/cyberpunk.js +36 -0
- package/dist/core/text/converters/index.js +261 -0
- package/dist/core/text/diff/cr2w.js +180 -0
- package/dist/core/text/diff/index.js +16 -0
- package/dist/core/text/diff/loca.js +43 -0
- package/dist/core/text/diff/lsx.js +126 -0
- package/dist/core/text/handles.js +86 -0
- package/dist/core/text/index.js +6 -0
- package/dist/core/text/json.js +46 -0
- package/dist/core/text/templates/cache.js +71 -0
- package/dist/core/text/templates/index.js +3 -0
- package/dist/core/text/templates/library.js +245 -0
- package/dist/core/text/templates/types.js +51 -0
- package/dist/core/text/toolchain.js +16 -0
- package/dist/core/text/types.js +1 -0
- package/dist/core/text/validate.js +149 -0
- package/dist/core/text/xml.js +58 -0
- package/dist/core/toolchain/datafetch.js +151 -0
- package/dist/core/toolchain/env.js +63 -0
- package/dist/core/toolchain/features.js +170 -0
- package/dist/core/toolchain/index.js +8 -0
- package/dist/core/toolchain/install.js +191 -0
- package/dist/core/toolchain/locate.js +275 -0
- package/dist/core/toolchain/remember.js +84 -0
- package/dist/core/toolchain/run.js +318 -0
- package/dist/core/toolchain/specs.js +351 -0
- package/dist/core/toolchain/types.js +1 -0
- package/dist/core/types.js +1 -0
- package/dist/core/userconfig.js +66 -0
- package/dist/core/validate/context.js +139 -0
- package/dist/core/validate/index.js +5 -0
- package/dist/core/validate/registry.js +58 -0
- package/dist/core/validate/run.js +131 -0
- package/dist/core/validate/suppressions.js +91 -0
- package/dist/core/validate/types.js +2 -0
- package/dist/core/yamledit.js +51 -0
- package/dist/index.js +389 -0
- package/dist/server.js +1914 -0
- package/dist/surfaces/baldursgate3/compat.js +164 -0
- package/dist/surfaces/baldursgate3/identity.js +110 -0
- package/dist/surfaces/baldursgate3/index/build.js +331 -0
- package/dist/surfaces/baldursgate3/index/index.js +17 -0
- package/dist/surfaces/baldursgate3/index/info.js +40 -0
- package/dist/surfaces/baldursgate3/index/parsers/loca.js +41 -0
- package/dist/surfaces/baldursgate3/index/parsers/lsx.js +137 -0
- package/dist/surfaces/baldursgate3/index/parsers/stats.js +45 -0
- package/dist/surfaces/baldursgate3/index/parsers/treasuretable.js +57 -0
- package/dist/surfaces/baldursgate3/index/parsers/xmlutil.js +142 -0
- package/dist/surfaces/baldursgate3/index/queries.js +327 -0
- package/dist/surfaces/baldursgate3/index/schema.js +62 -0
- package/dist/surfaces/baldursgate3/index.js +3 -0
- package/dist/surfaces/baldursgate3/logs.js +278 -0
- package/dist/surfaces/baldursgate3/modsettings.js +115 -0
- package/dist/surfaces/baldursgate3/pak.js +153 -0
- package/dist/surfaces/baldursgate3/settings.js +49 -0
- package/dist/surfaces/baldursgate3/surface.js +232 -0
- package/dist/surfaces/baldursgate3/validators/bridge.js +7 -0
- package/dist/surfaces/baldursgate3/validators/files.js +476 -0
- package/dist/surfaces/baldursgate3/validators/items.js +1001 -0
- package/dist/surfaces/baldursgate3/validators/progression.js +734 -0
- package/dist/surfaces/baldursgate3/validators/stats.js +1076 -0
- package/dist/surfaces/baldursgate3/validators/story.js +790 -0
- package/dist/surfaces/baldursgate3/validators/templates.js +7 -0
- package/dist/surfaces/cyberpunk2077/compat.js +223 -0
- package/dist/surfaces/cyberpunk2077/index/build.js +231 -0
- package/dist/surfaces/cyberpunk2077/index/index.js +17 -0
- package/dist/surfaces/cyberpunk2077/index/info.js +51 -0
- package/dist/surfaces/cyberpunk2077/index/model.js +52 -0
- package/dist/surfaces/cyberpunk2077/index/parsers/tweak.js +483 -0
- package/dist/surfaces/cyberpunk2077/index/parsers/tweakxl-yaml.js +183 -0
- package/dist/surfaces/cyberpunk2077/index/queries.js +504 -0
- package/dist/surfaces/cyberpunk2077/index/resource.js +135 -0
- package/dist/surfaces/cyberpunk2077/index/schema.js +90 -0
- package/dist/surfaces/cyberpunk2077/index.js +3 -0
- package/dist/surfaces/cyberpunk2077/logs.js +742 -0
- package/dist/surfaces/cyberpunk2077/paths.js +12 -0
- package/dist/surfaces/cyberpunk2077/surface.js +218 -0
- package/dist/surfaces/cyberpunk2077/validators/bridge.js +7 -0
- package/dist/surfaces/cyberpunk2077/validators/cr2w.js +349 -0
- package/dist/surfaces/cyberpunk2077/validators/garments.js +1181 -0
- package/dist/surfaces/cyberpunk2077/validators/mesh.js +338 -0
- package/dist/surfaces/cyberpunk2077/validators/modsettings.js +120 -0
- package/dist/surfaces/cyberpunk2077/validators/packaging.js +463 -0
- package/dist/surfaces/cyberpunk2077/validators/red4ext.js +318 -0
- package/dist/surfaces/cyberpunk2077/validators/redscript.js +193 -0
- package/dist/surfaces/cyberpunk2077/validators/templates.js +7 -0
- package/dist/surfaces/cyberpunk2077/validators/tweaks.js +768 -0
- package/dist/surfaces/cyberpunk2077/validators/vehicles.js +531 -0
- package/dist/surfaces/cyberpunk2077/validators/xl.js +308 -0
- package/dist/surfaces/eldenring/binders.js +111 -0
- package/dist/surfaces/eldenring/compat.js +166 -0
- package/dist/surfaces/eldenring/dcx.js +49 -0
- package/dist/surfaces/eldenring/index.js +13 -0
- package/dist/surfaces/eldenring/loaders.js +132 -0
- package/dist/surfaces/eldenring/logs.js +67 -0
- package/dist/surfaces/eldenring/paramdef.js +256 -0
- package/dist/surfaces/eldenring/params.js +129 -0
- package/dist/surfaces/eldenring/surface.js +121 -0
- package/dist/surfaces/eldenring/tools.js +52 -0
- package/dist/surfaces/eldenring/validators/binders.js +193 -0
- package/dist/surfaces/eldenring/validators/packages.js +159 -0
- package/dist/surfaces/eldenring/validators/params.js +334 -0
- package/dist/surfaces/eldenring/validators/profiles.js +287 -0
- package/dist/surfaces/index.js +34 -0
- package/dist/surfaces/nivalisnights/compat.js +229 -0
- package/dist/surfaces/nivalisnights/index/build.js +310 -0
- package/dist/surfaces/nivalisnights/index/index.js +5 -0
- package/dist/surfaces/nivalisnights/index/queries.js +231 -0
- package/dist/surfaces/nivalisnights/index.js +4 -0
- package/dist/surfaces/nivalisnights/logs.js +473 -0
- package/dist/surfaces/nivalisnights/surface.js +195 -0
- package/dist/surfaces/skyrimse/archives.js +229 -0
- package/dist/surfaces/skyrimse/compat.js +163 -0
- package/dist/surfaces/skyrimse/index.js +12 -0
- package/dist/surfaces/skyrimse/loadorder.js +174 -0
- package/dist/surfaces/skyrimse/logs.js +241 -0
- package/dist/surfaces/skyrimse/plugins.js +149 -0
- package/dist/surfaces/skyrimse/records.js +94 -0
- package/dist/surfaces/skyrimse/surface.js +240 -0
- package/dist/surfaces/skyrimse/validators/archive-records.js +165 -0
- package/dist/surfaces/skyrimse/validators/archives.js +110 -0
- package/dist/surfaces/skyrimse/validators/plugins.js +374 -0
- package/dist/surfaces/skyrimse/validators/records.js +117 -0
- package/dist/surfaces/stardewvalley/compat.js +190 -0
- package/dist/surfaces/stardewvalley/config.js +42 -0
- package/dist/surfaces/stardewvalley/contentpatcher.js +203 -0
- package/dist/surfaces/stardewvalley/index/index.js +229 -0
- package/dist/surfaces/stardewvalley/index/wiki.js +190 -0
- package/dist/surfaces/stardewvalley/index.js +12 -0
- package/dist/surfaces/stardewvalley/logs.js +307 -0
- package/dist/surfaces/stardewvalley/mods.js +182 -0
- package/dist/surfaces/stardewvalley/surface.js +244 -0
- package/dist/surfaces/stardewvalley/validators/content.js +321 -0
- package/dist/surfaces/stardewvalley/validators/fields.js +147 -0
- package/dist/surfaces/stardewvalley/validators/loadorder.js +69 -0
- package/dist/surfaces/stardewvalley/validators/manifest.js +221 -0
- package/dist/surfaces/subnautica2/compat.js +138 -0
- package/dist/surfaces/subnautica2/index.js +5 -0
- package/dist/surfaces/subnautica2/logs.js +129 -0
- package/dist/surfaces/subnautica2/paks.js +173 -0
- package/dist/surfaces/subnautica2/surface.js +151 -0
- package/dist/surfaces/subnautica2/validators/packaging.js +298 -0
- package/dist/surfaces/valheim/compat.js +171 -0
- package/dist/surfaces/valheim/index/index.js +301 -0
- package/dist/surfaces/valheim/index/tables.js +103 -0
- package/dist/surfaces/valheim/index.js +11 -0
- package/dist/surfaces/valheim/logs.js +197 -0
- package/dist/surfaces/valheim/packaging.js +131 -0
- package/dist/surfaces/valheim/plugins.js +270 -0
- package/dist/surfaces/valheim/references.js +121 -0
- package/dist/surfaces/valheim/surface.js +223 -0
- package/dist/surfaces/valheim/validators/packaging.js +218 -0
- package/dist/surfaces/valheim/validators/plugins.js +287 -0
- package/dist/surfaces/valheim/validators/references.js +169 -0
- package/knowledge/baldursgate3/animations.gr2.yaml +166 -0
- package/knowledge/baldursgate3/audio.voice.yaml +182 -0
- package/knowledge/baldursgate3/audio.wwise.yaml +104 -0
- package/knowledge/baldursgate3/charactercreation.heads-hair.yaml +193 -0
- package/knowledge/baldursgate3/communitylibrary.shared-content.yaml +129 -0
- package/knowledge/baldursgate3/compatibilityframework.api.yaml +176 -0
- package/knowledge/baldursgate3/crash.causes.yaml +90 -0
- package/knowledge/baldursgate3/dialogue.dialogbank.yaml +120 -0
- package/knowledge/baldursgate3/dialogue.files.yaml +236 -0
- package/knowledge/baldursgate3/dialogue.osiris-goals.yaml +386 -0
- package/knowledge/baldursgate3/dialogue.yaml +122 -0
- package/knowledge/baldursgate3/feats.classes.yaml +164 -0
- package/knowledge/baldursgate3/items.cloth-physics.yaml +167 -0
- package/knowledge/baldursgate3/items.roottemplates.yaml +320 -0
- package/knowledge/baldursgate3/items.visualbank.yaml +628 -0
- package/knowledge/baldursgate3/journal.quests.yaml +214 -0
- package/knowledge/baldursgate3/loadorder.modsettings.yaml +128 -0
- package/knowledge/baldursgate3/localization.handles.yaml +92 -0
- package/knowledge/baldursgate3/misc.encoding-and-practice.yaml +111 -0
- package/knowledge/baldursgate3/origins.companions.yaml +144 -0
- package/knowledge/baldursgate3/osiris.signatures.yaml +211 -0
- package/knowledge/baldursgate3/packaging.meta.yaml +155 -0
- package/knowledge/baldursgate3/packaging.paks.yaml +79 -0
- package/knowledge/baldursgate3/process.launch.yaml +34 -0
- package/knowledge/baldursgate3/progressions.yaml +92 -0
- package/knowledge/baldursgate3/publishing.modio-nexus.yaml +170 -0
- package/knowledge/baldursgate3/publishing.toolkit-modio.yaml +135 -0
- package/knowledge/baldursgate3/races.progression.yaml +228 -0
- package/knowledge/baldursgate3/scriptextender.console.yaml +257 -0
- package/knowledge/baldursgate3/scriptextender.yaml +252 -0
- package/knowledge/baldursgate3/stats.boosts.yaml +293 -0
- package/knowledge/baldursgate3/stats.equipment-kits.yaml +69 -0
- package/knowledge/baldursgate3/stats.interrupts.yaml +45 -0
- package/knowledge/baldursgate3/stats.itemcombos.yaml +65 -0
- package/knowledge/baldursgate3/stats.spells.yaml +293 -0
- package/knowledge/baldursgate3/stats.weapons.yaml +210 -0
- package/knowledge/baldursgate3/toolchain.divine.yaml +179 -0
- package/knowledge/baldursgate3/toolchain.lslib-formats.yaml +242 -0
- package/knowledge/baldursgate3/toolkit.levels.yaml +177 -0
- package/knowledge/baldursgate3/ui.icons-atlas.yaml +78 -0
- package/knowledge/baldursgate3/ui.icons.yaml +412 -0
- package/knowledge/baldursgate3/ui.mcm.yaml +161 -0
- package/knowledge/baldursgate3/vfx.materials.yaml +174 -0
- package/knowledge/baldursgate3/visuals.characterfix.yaml +73 -0
- package/knowledge/cyberpunk2077/animations.anims.yaml +118 -0
- package/knowledge/cyberpunk2077/anims.archivexl.yaml +163 -0
- package/knowledge/cyberpunk2077/anims.authoring.yaml +579 -0
- package/knowledge/cyberpunk2077/anims.encoder.yaml +156 -0
- package/knowledge/cyberpunk2077/anims.graph.yaml +629 -0
- package/knowledge/cyberpunk2077/anims.method.yaml +360 -0
- package/knowledge/cyberpunk2077/anims.pipeline.yaml +352 -0
- package/knowledge/cyberpunk2077/anims.roundtrip.yaml +222 -0
- package/knowledge/cyberpunk2077/anims.sets.yaml +176 -0
- package/knowledge/cyberpunk2077/archivexl.bodytypes.yaml +149 -0
- package/knowledge/cyberpunk2077/archivexl.dynamic-appearances.yaml +218 -0
- package/knowledge/cyberpunk2077/archivexl.manifest.yaml +214 -0
- package/knowledge/cyberpunk2077/audio.events.yaml +168 -0
- package/knowledge/cyberpunk2077/audio.sounds.yaml +164 -0
- package/knowledge/cyberpunk2077/audio.voicesets.yaml +146 -0
- package/knowledge/cyberpunk2077/cet.sandbox.yaml +219 -0
- package/knowledge/cyberpunk2077/clothing.garments.yaml +283 -0
- package/knowledge/cyberpunk2077/clothing.refits.yaml +215 -0
- package/knowledge/cyberpunk2077/codeware.overview.yaml +111 -0
- package/knowledge/cyberpunk2077/codeware.systems.yaml +192 -0
- package/knowledge/cyberpunk2077/drones.combat.yaml +416 -0
- package/knowledge/cyberpunk2077/drones.control.yaml +552 -0
- package/knowledge/cyberpunk2077/entities.appearance.yaml +166 -0
- package/knowledge/cyberpunk2077/equipmentex.slots.yaml +151 -0
- package/knowledge/cyberpunk2077/fx.effects.yaml +246 -0
- package/knowledge/cyberpunk2077/fx.lights.yaml +171 -0
- package/knowledge/cyberpunk2077/hair.modding.yaml +145 -0
- package/knowledge/cyberpunk2077/items.wiring.yaml +246 -0
- package/knowledge/cyberpunk2077/loadorder.archives.yaml +71 -0
- package/knowledge/cyberpunk2077/mesh.blender.yaml +209 -0
- package/knowledge/cyberpunk2077/nativedb.dump.yaml +98 -0
- package/knowledge/cyberpunk2077/npc.appearance.yaml +275 -0
- package/knowledge/cyberpunk2077/npc.behaviour.yaml +128 -0
- package/knowledge/cyberpunk2077/npc.locomotion.yaml +490 -0
- package/knowledge/cyberpunk2077/packaging.deploy.yaml +175 -0
- package/knowledge/cyberpunk2077/packaging.variants.yaml +176 -0
- package/knowledge/cyberpunk2077/player.control.yaml +918 -0
- package/knowledge/cyberpunk2077/process.assetbuild.yaml +154 -0
- package/knowledge/cyberpunk2077/process.lessons.yaml +276 -0
- package/knowledge/cyberpunk2077/quests.minor.yaml +46 -0
- package/knowledge/cyberpunk2077/quests.scenes.yaml +282 -0
- package/knowledge/cyberpunk2077/quickhacks.tiers.yaml +41 -0
- package/knowledge/cyberpunk2077/red4ext.api.yaml +645 -0
- package/knowledge/cyberpunk2077/red4ext.plugins.yaml +124 -0
- package/knowledge/cyberpunk2077/redhottools.hotreload.yaml +245 -0
- package/knowledge/cyberpunk2077/redscript.compile-log.yaml +76 -0
- package/knowledge/cyberpunk2077/redscript.modules.yaml +118 -0
- package/knowledge/cyberpunk2077/toolchain.wolvenkit.yaml +163 -0
- package/knowledge/cyberpunk2077/tweakdb.loot-recipes.yaml +184 -0
- package/knowledge/cyberpunk2077/tweakdb.melee.yaml +352 -0
- package/knowledge/cyberpunk2077/tweakdb.weapons.yaml +325 -0
- package/knowledge/cyberpunk2077/tweakxl.hotreload.yaml +151 -0
- package/knowledge/cyberpunk2077/tweakxl.yaml-syntax.yaml +208 -0
- package/knowledge/cyberpunk2077/ui.icons.yaml +259 -0
- package/knowledge/cyberpunk2077/ui.inkatlas.yaml +81 -0
- package/knowledge/cyberpunk2077/ui.modsettings.yaml +204 -0
- package/knowledge/cyberpunk2077/vehicles.control.yaml +475 -0
- package/knowledge/cyberpunk2077/vehicles.dashboard-ui.yaml +203 -0
- package/knowledge/cyberpunk2077/vehicles.entities.yaml +195 -0
- package/knowledge/cyberpunk2077/vehicles.wiring.yaml +137 -0
- package/knowledge/cyberpunk2077/wolvenkit.cli.yaml +167 -0
- package/knowledge/cyberpunk2077/workspot.system.yaml +55 -0
- package/knowledge/cyberpunk2077/world.interactions.yaml +41 -0
- package/knowledge/cyberpunk2077/world.loot-container.yaml +167 -0
- package/knowledge/cyberpunk2077/world.sectors.yaml +233 -0
- package/knowledge/eldenring/formats.binders.yaml +237 -0
- package/knowledge/eldenring/launcher.me3.yaml +230 -0
- package/knowledge/eldenring/launcher.modengine2.yaml +119 -0
- package/knowledge/eldenring/params.paramdef.yaml +149 -0
- package/knowledge/eldenring/params.regulation.yaml +142 -0
- package/knowledge/eldenring/saves.online.yaml +118 -0
- package/knowledge/eldenring/toolchain.cli.yaml +256 -0
- package/knowledge/nivalisnights/modding.runtime.yaml +157 -0
- package/knowledge/skyrimse/archives.bsa.yaml +212 -0
- package/knowledge/skyrimse/install.detection.yaml +41 -0
- package/knowledge/skyrimse/loadorder.files.yaml +242 -0
- package/knowledge/skyrimse/plugins.esl.yaml +189 -0
- package/knowledge/skyrimse/plugins.format.yaml +224 -0
- package/knowledge/skyrimse/skse.runtime.yaml +283 -0
- package/knowledge/skyrimse/toolchain.xedit-spriggit.yaml +336 -0
- package/knowledge/stardewvalley/contentpatcher.format.yaml +338 -0
- package/knowledge/stardewvalley/distribution.channels.yaml +236 -0
- package/knowledge/stardewvalley/game.versions.yaml +202 -0
- package/knowledge/stardewvalley/install.layout.yaml +277 -0
- package/knowledge/stardewvalley/smapi.loader.yaml +284 -0
- package/knowledge/stardewvalley/smapi.logs.yaml +329 -0
- package/knowledge/stardewvalley/smapi.manifest.yaml +293 -0
- package/knowledge/stardewvalley/toolchain.build.yaml +261 -0
- package/knowledge/stardewvalley/wiki.dataformat.yaml +130 -0
- package/knowledge/subnautica2/game.build.yaml +331 -0
- package/knowledge/subnautica2/modding.state.yaml +210 -0
- package/knowledge/subnautica2/ue5.containers.yaml +72 -0
- package/knowledge/subnautica2/ue5.paks.yaml +203 -0
- package/knowledge/subnautica2/ue5.tooling.yaml +175 -0
- package/knowledge/subnautica2/ue5.ue4ss.yaml +203 -0
- package/knowledge/valheim/bepinex.loader.yaml +247 -0
- package/knowledge/valheim/bepinex.plugins.yaml +251 -0
- package/knowledge/valheim/game.versions.yaml +321 -0
- package/knowledge/valheim/install.layout.yaml +216 -0
- package/knowledge/valheim/jotunn.library.yaml +334 -0
- package/knowledge/valheim/thunderstore.packaging.yaml +217 -0
- package/knowledge/valheim/toolchain.build.yaml +111 -0
- package/package.json +29 -0
- package/scaffolds/baldursgate3/new-class-injection.yaml +148 -0
- package/scaffolds/baldursgate3/new-item.yaml +264 -0
- package/scaffolds/baldursgate3/new-mod.yaml +174 -0
- package/scaffolds/baldursgate3/new-passive.yaml +100 -0
- package/scaffolds/baldursgate3/new-project.yaml +118 -0
- package/scaffolds/baldursgate3/new-quest-stub.yaml +159 -0
- package/scaffolds/baldursgate3/new-spell.yaml +102 -0
- package/scaffolds/baldursgate3/new-status.yaml +119 -0
- package/scaffolds/baldursgate3/sequences/subclass-kit.yaml +50 -0
- package/scaffolds/cyberpunk2077/edit-tweak.yaml +70 -0
- package/scaffolds/cyberpunk2077/mesh-export-preset.yaml +170 -0
- package/scaffolds/cyberpunk2077/new-entity-patch.yaml +255 -0
- package/scaffolds/cyberpunk2077/new-garment-refit.yaml +279 -0
- package/scaffolds/cyberpunk2077/new-item-chain.yaml +548 -0
- package/scaffolds/cyberpunk2077/new-localization.yaml +130 -0
- package/scaffolds/cyberpunk2077/new-mod-settings.yaml +133 -0
- package/scaffolds/cyberpunk2077/new-player-replacer.yaml +1003 -0
- package/scaffolds/cyberpunk2077/new-project.yaml +157 -0
- package/scaffolds/cyberpunk2077/new-sector-node.yaml +156 -0
- package/scaffolds/cyberpunk2077/new-tweak.yaml +121 -0
- package/scaffolds/cyberpunk2077/new-vehicle-livery.yaml +148 -0
- package/scaffolds/cyberpunk2077/new-workspot-entity.yaml +177 -0
- package/scaffolds/cyberpunk2077/sequences/iconic-weapon.yaml +72 -0
- package/scaffolds/cyberpunk2077/sequences/quickhack-takeover.yaml +213 -0
- package/scaffolds/stardewvalley/new-cp-patch.yaml +106 -0
- package/scaffolds/stardewvalley/new-csharp-mod.yaml +195 -0
- package/scaffolds/stardewvalley/new-project.yaml +184 -0
- package/scaffolds/stardewvalley/sequences/content-pack.yaml +31 -0
- package/scripts/mw-tool.mjs +184 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { promises as fs } from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { StringDecoder } from "node:string_decoder";
|
|
5
|
+
import { pathExists } from "../fsutil.js";
|
|
6
|
+
import { userDataDir } from "../home.js";
|
|
7
|
+
import { loadUserConfig } from "../userconfig.js";
|
|
8
|
+
import { toolEnvironment } from "./env.js";
|
|
9
|
+
import { TOOL_SPECS } from "./specs.js";
|
|
10
|
+
import { batchFileArgv, isBatchFile } from "./run.js";
|
|
11
|
+
const VERSION_PROBE_TIMEOUT_MS = 5_000;
|
|
12
|
+
function pathEntries() {
|
|
13
|
+
const raw = process.env.PATH ?? process.env.Path ?? "";
|
|
14
|
+
return raw.split(path.delimiter).filter((entry) => entry.length > 0);
|
|
15
|
+
}
|
|
16
|
+
function pathExt() {
|
|
17
|
+
if (process.platform !== "win32")
|
|
18
|
+
return [""];
|
|
19
|
+
const raw = process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD";
|
|
20
|
+
return raw.split(";").map((ext) => ext.toLowerCase());
|
|
21
|
+
}
|
|
22
|
+
function executableMatchesEntry(name, entry) {
|
|
23
|
+
if (process.platform !== "win32")
|
|
24
|
+
return entry === name;
|
|
25
|
+
const entryLower = entry.toLowerCase();
|
|
26
|
+
const nameLower = name.toLowerCase();
|
|
27
|
+
if (entryLower === nameLower)
|
|
28
|
+
return true;
|
|
29
|
+
const ext = path.extname(nameLower);
|
|
30
|
+
if (ext)
|
|
31
|
+
return false;
|
|
32
|
+
return pathExt().some((candidateExt) => entryLower === nameLower + candidateExt);
|
|
33
|
+
}
|
|
34
|
+
async function findOnPath(spec) {
|
|
35
|
+
for (const dir of pathEntries()) {
|
|
36
|
+
let entries;
|
|
37
|
+
try {
|
|
38
|
+
entries = await fs.readdir(dir);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
for (const name of spec.executables) {
|
|
44
|
+
const hit = entries.find((entry) => executableMatchesEntry(name, entry));
|
|
45
|
+
if (hit)
|
|
46
|
+
return path.join(dir, hit);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
function naturalCompare(a, b) {
|
|
52
|
+
const chunk = /(\d+|\D+)/g;
|
|
53
|
+
const aParts = a.match(chunk) ?? [a];
|
|
54
|
+
const bParts = b.match(chunk) ?? [b];
|
|
55
|
+
const len = Math.max(aParts.length, bParts.length);
|
|
56
|
+
for (let i = 0; i < len; i++) {
|
|
57
|
+
const av = aParts[i] ?? "";
|
|
58
|
+
const bv = bParts[i] ?? "";
|
|
59
|
+
if (av === bv)
|
|
60
|
+
continue;
|
|
61
|
+
const an = Number(av);
|
|
62
|
+
const bn = Number(bv);
|
|
63
|
+
if (!Number.isNaN(an) && !Number.isNaN(bn))
|
|
64
|
+
return an - bn;
|
|
65
|
+
return av < bv ? -1 : 1;
|
|
66
|
+
}
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
async function resolveOneCandidate(template) {
|
|
70
|
+
const normalized = template.replace(/\\/g, "/");
|
|
71
|
+
const segments = normalized.split("/");
|
|
72
|
+
const starIndex = segments.findIndex((segment) => segment.includes("*"));
|
|
73
|
+
if (starIndex === -1) {
|
|
74
|
+
return (await pathExists(normalized)) ? normalized : undefined;
|
|
75
|
+
}
|
|
76
|
+
const dir = segments.slice(0, starIndex).join("/") || "/";
|
|
77
|
+
const rest = segments.slice(starIndex + 1);
|
|
78
|
+
const patternSegment = segments[starIndex] ?? "*";
|
|
79
|
+
const regex = new RegExp("^" + patternSegment.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*") + "$", "i");
|
|
80
|
+
let entries;
|
|
81
|
+
try {
|
|
82
|
+
entries = await fs.readdir(dir);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
const matches = entries.filter((entry) => regex.test(entry)).sort(naturalCompare).reverse();
|
|
88
|
+
for (const match of matches) {
|
|
89
|
+
const candidate = [dir, match, ...rest].join("/");
|
|
90
|
+
const resolved = await resolveOneCandidate(candidate);
|
|
91
|
+
if (resolved)
|
|
92
|
+
return resolved;
|
|
93
|
+
}
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
const probeCache = new Map();
|
|
97
|
+
async function probeVersion(execPath, spec) {
|
|
98
|
+
const versionArgs = spec.versionArgs;
|
|
99
|
+
const versionPattern = spec.versionPattern;
|
|
100
|
+
if (!versionArgs || !versionPattern)
|
|
101
|
+
return "unknown";
|
|
102
|
+
let key;
|
|
103
|
+
try {
|
|
104
|
+
const stat = await fs.stat(execPath);
|
|
105
|
+
key = JSON.stringify([execPath, stat.size, stat.mtimeMs, versionArgs, versionPattern.source]);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
key = undefined;
|
|
109
|
+
}
|
|
110
|
+
if (key === undefined)
|
|
111
|
+
return runVersionProbe(execPath, versionArgs, versionPattern);
|
|
112
|
+
let cached = probeCache.get(key);
|
|
113
|
+
if (!cached) {
|
|
114
|
+
cached = runVersionProbe(execPath, versionArgs, versionPattern);
|
|
115
|
+
probeCache.set(key, cached);
|
|
116
|
+
const settledKey = key;
|
|
117
|
+
void cached.then((version) => {
|
|
118
|
+
if (version === "unknown")
|
|
119
|
+
probeCache.delete(settledKey);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return cached;
|
|
123
|
+
}
|
|
124
|
+
async function runVersionProbe(execPath, versionArgs, versionPattern) {
|
|
125
|
+
const output = await new Promise((resolve) => {
|
|
126
|
+
let settled = false;
|
|
127
|
+
let child;
|
|
128
|
+
try {
|
|
129
|
+
const env = toolEnvironment().env;
|
|
130
|
+
const shim = process.platform === "win32" && isBatchFile(execPath) ? batchFileArgv(execPath, versionArgs, env) : undefined;
|
|
131
|
+
child = shim
|
|
132
|
+
? spawn(shim.command, shim.argv, { stdio: ["ignore", "pipe", "pipe"], env, windowsVerbatimArguments: true })
|
|
133
|
+
: spawn(execPath, versionArgs, { stdio: ["ignore", "pipe", "pipe"], env });
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
resolve("");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
let out = "";
|
|
140
|
+
const finish = (result) => {
|
|
141
|
+
if (settled)
|
|
142
|
+
return;
|
|
143
|
+
settled = true;
|
|
144
|
+
clearTimeout(timer);
|
|
145
|
+
resolve(result);
|
|
146
|
+
};
|
|
147
|
+
const timer = setTimeout(() => {
|
|
148
|
+
child.kill();
|
|
149
|
+
finish(out);
|
|
150
|
+
}, VERSION_PROBE_TIMEOUT_MS);
|
|
151
|
+
const outDecoder = new StringDecoder("utf8");
|
|
152
|
+
const errDecoder = new StringDecoder("utf8");
|
|
153
|
+
child.stdout?.on("data", (chunk) => (out += outDecoder.write(chunk)));
|
|
154
|
+
child.stderr?.on("data", (chunk) => (out += errDecoder.write(chunk)));
|
|
155
|
+
child.on("error", () => finish(out));
|
|
156
|
+
child.on("close", () => finish(out));
|
|
157
|
+
});
|
|
158
|
+
const match = versionPattern.exec(output);
|
|
159
|
+
return match?.[1] ?? "unknown";
|
|
160
|
+
}
|
|
161
|
+
async function resolveOverride(override, spec) {
|
|
162
|
+
let stat;
|
|
163
|
+
try {
|
|
164
|
+
stat = await fs.stat(override);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
if (!stat.isDirectory())
|
|
170
|
+
return override;
|
|
171
|
+
let entries;
|
|
172
|
+
try {
|
|
173
|
+
entries = await fs.readdir(override);
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
for (const name of spec.executables) {
|
|
179
|
+
const hit = entries.find((entry) => executableMatchesEntry(name, entry));
|
|
180
|
+
if (hit)
|
|
181
|
+
return path.join(override, hit);
|
|
182
|
+
}
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
async function findManaged(spec) {
|
|
186
|
+
const root = path.join(userDataDir(), "tools", spec.id);
|
|
187
|
+
let entries;
|
|
188
|
+
try {
|
|
189
|
+
entries = (await fs.readdir(root, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
for (const dir of [...entries.sort(naturalCompare).reverse().map((v) => path.join(root, v)), root]) {
|
|
195
|
+
for (const sub of [dir, path.join(dir, "bin"), path.join(dir, "Scripts")]) {
|
|
196
|
+
const hit = await resolveOverride(sub, spec);
|
|
197
|
+
if (hit && hit !== sub)
|
|
198
|
+
return hit;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
export async function resolveToolPath(spec, given) {
|
|
204
|
+
return resolveOverride(given, spec);
|
|
205
|
+
}
|
|
206
|
+
export async function locateTool(spec, options = {}) {
|
|
207
|
+
const base = { id: spec.id, label: spec.label, usedBy: spec.usedBy };
|
|
208
|
+
const policy = options.probe ?? "all";
|
|
209
|
+
const found = async (execPath, source) => {
|
|
210
|
+
if (policy === "none") {
|
|
211
|
+
return { ...base, found: true, path: execPath, source, versionNotProbed: "this is a dry-run, which never runs a tool" };
|
|
212
|
+
}
|
|
213
|
+
if (policy === "trusted" && source === "project") {
|
|
214
|
+
return {
|
|
215
|
+
...base,
|
|
216
|
+
found: true,
|
|
217
|
+
path: execPath,
|
|
218
|
+
source,
|
|
219
|
+
versionNotProbed: "a project toolchain override is only run by a build or another apply, never by a read-only check",
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
return { ...base, found: true, path: execPath, source, version: await probeVersion(execPath, spec) };
|
|
223
|
+
};
|
|
224
|
+
const envPath = process.env[spec.envVar];
|
|
225
|
+
const fromEnv = envPath ? await resolveOverride(envPath, spec) : undefined;
|
|
226
|
+
if (fromEnv)
|
|
227
|
+
return found(fromEnv, "env");
|
|
228
|
+
const override = options.projectOverrides?.[spec.id];
|
|
229
|
+
const fromProject = override ? await resolveOverride(override, spec) : undefined;
|
|
230
|
+
if (fromProject)
|
|
231
|
+
return found(fromProject, "project");
|
|
232
|
+
const userTools = options.userTools === undefined ? (await loadUserConfig()).config.tools : options.userTools;
|
|
233
|
+
const remembered = userTools?.[spec.id];
|
|
234
|
+
const fromUser = remembered ? await resolveOverride(remembered.path, spec) : undefined;
|
|
235
|
+
if (fromUser)
|
|
236
|
+
return found(fromUser, remembered.installedBy === "modwright" ? "managed" : "user");
|
|
237
|
+
const managed = await findManaged(spec);
|
|
238
|
+
if (managed)
|
|
239
|
+
return found(managed, "managed");
|
|
240
|
+
const onPath = await findOnPath(spec);
|
|
241
|
+
if (onPath)
|
|
242
|
+
return found(onPath, "path");
|
|
243
|
+
for (const candidate of spec.candidates) {
|
|
244
|
+
const resolved = await resolveOneCandidate(candidate);
|
|
245
|
+
if (resolved)
|
|
246
|
+
return found(resolved, "candidate");
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
...base,
|
|
250
|
+
found: false,
|
|
251
|
+
hint: hintFor(spec, remembered),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
function hintFor(spec, staleEntry) {
|
|
255
|
+
const stale = staleEntry ? ` The path remembered in ModWright's config (${staleEntry.path}) no longer holds it.` : "";
|
|
256
|
+
const parts = [
|
|
257
|
+
...(spec.managed ? [`ModWright can install it: check_toolchain action=install tool=${spec.id} (a dry run first shows the file, source, size and sha256)`] : []),
|
|
258
|
+
`${spec.managed ? "or point" : "point"} ModWright at an existing copy once with check_toolchain action=remember tool=${spec.id} toolPath=<path>`,
|
|
259
|
+
`or set ${spec.envVar}`,
|
|
260
|
+
];
|
|
261
|
+
if (spec.homepage && !spec.managed)
|
|
262
|
+
parts.push(`or install it from ${spec.homepage}`);
|
|
263
|
+
return `${spec.label} was not found on PATH or in common install locations.${stale} To fix: ${parts.join(", ")}.`;
|
|
264
|
+
}
|
|
265
|
+
export async function checkToolchain(options = {}) {
|
|
266
|
+
const specs = options.surfaceId
|
|
267
|
+
? TOOL_SPECS.filter((spec) => spec.usedBy.includes(options.surfaceId))
|
|
268
|
+
: TOOL_SPECS;
|
|
269
|
+
const results = [];
|
|
270
|
+
const userTools = options.userTools === undefined ? (await loadUserConfig()).config.tools : options.userTools;
|
|
271
|
+
for (const spec of specs) {
|
|
272
|
+
results.push(await locateTool(spec, { projectOverrides: options.projectOverrides, probe: options.probe ?? "trusted", userTools }));
|
|
273
|
+
}
|
|
274
|
+
return results;
|
|
275
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { loadUserConfig, userConfigFile, userConfigWritePlan } from "../userconfig.js";
|
|
4
|
+
import { resolveToolPath } from "./locate.js";
|
|
5
|
+
import { TOOL_SPECS } from "./specs.js";
|
|
6
|
+
export async function planDataMemory(action, dataId, dataPath, known, options = {}) {
|
|
7
|
+
const need = known.find((k) => k.id === dataId);
|
|
8
|
+
if (!need)
|
|
9
|
+
return { ok: false, error: `Unknown data folder "${dataId}". Known: ${known.map((k) => `${k.id} (${k.what})`).join(", ") || "none"}.` };
|
|
10
|
+
const file = options.file ?? userConfigFile();
|
|
11
|
+
const loaded = await loadUserConfig(file);
|
|
12
|
+
if (loaded.problems.length > 0)
|
|
13
|
+
return { ok: false, error: `${loaded.problems.join(" ")} Fix or remove ${file} before changing it through ModWright.` };
|
|
14
|
+
const previous = loaded.config.data[need.id];
|
|
15
|
+
if (action === "forget") {
|
|
16
|
+
if (!previous)
|
|
17
|
+
return { ok: false, error: `${need.id} is not remembered in ${file}; nothing to forget.` };
|
|
18
|
+
const { [need.id]: _dropped, ...rest } = loaded.config.data;
|
|
19
|
+
const config = { ...loaded.config, data: rest };
|
|
20
|
+
return { ok: true, action, data: need.id, file, plan: userConfigWritePlan(config, `forget the ${need.id} data folder in the per-user config`, file), config, previous };
|
|
21
|
+
}
|
|
22
|
+
if (!dataPath)
|
|
23
|
+
return { ok: false, error: `remember needs dataPath: ${need.what}.` };
|
|
24
|
+
if (!path.isAbsolute(dataPath))
|
|
25
|
+
return { ok: false, error: `dataPath must be absolute ("${dataPath}" is relative): the config is read from every session, whatever its working directory.` };
|
|
26
|
+
let isDir = false;
|
|
27
|
+
try {
|
|
28
|
+
isDir = (await fs.stat(dataPath)).isDirectory();
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
}
|
|
32
|
+
if (!isDir)
|
|
33
|
+
return { ok: false, error: `${dataPath} is not an existing folder; ${need.id} should be ${need.what}.` };
|
|
34
|
+
const entry = { path: path.resolve(dataPath), rememberedAt: (options.now ?? (() => new Date()))().toISOString() };
|
|
35
|
+
const config = { ...loaded.config, data: { ...loaded.config.data, [need.id]: entry } };
|
|
36
|
+
return {
|
|
37
|
+
ok: true,
|
|
38
|
+
action,
|
|
39
|
+
data: need.id,
|
|
40
|
+
file,
|
|
41
|
+
plan: userConfigWritePlan(config, `remember ${dataPath} as the ${need.id} data folder in the per-user config`, file),
|
|
42
|
+
config,
|
|
43
|
+
entry,
|
|
44
|
+
...(previous ? { previous } : {}),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export async function planToolMemory(action, toolId, toolPath, options = {}) {
|
|
48
|
+
const spec = TOOL_SPECS.find((s) => s.id === toolId);
|
|
49
|
+
if (!spec)
|
|
50
|
+
return { ok: false, error: `Unknown tool "${toolId}". Known: ${TOOL_SPECS.map((s) => s.id).join(", ")}.` };
|
|
51
|
+
const file = options.file ?? userConfigFile();
|
|
52
|
+
const loaded = await loadUserConfig(file);
|
|
53
|
+
if (loaded.problems.length > 0) {
|
|
54
|
+
return { ok: false, error: `${loaded.problems.join(" ")} Fix or remove ${file} before changing it through ModWright.` };
|
|
55
|
+
}
|
|
56
|
+
const previous = loaded.config.tools[spec.id];
|
|
57
|
+
if (action === "forget") {
|
|
58
|
+
if (!previous)
|
|
59
|
+
return { ok: false, error: `${spec.id} is not remembered in ${file}; nothing to forget.` };
|
|
60
|
+
const { [spec.id]: _dropped, ...rest } = loaded.config.tools;
|
|
61
|
+
const config = { ...loaded.config, tools: rest };
|
|
62
|
+
return { ok: true, action, tool: spec.id, file, plan: userConfigWritePlan(config, `forget ${spec.label} in the per-user config`, file), config, previous };
|
|
63
|
+
}
|
|
64
|
+
if (!toolPath)
|
|
65
|
+
return { ok: false, error: `remember needs toolPath: the ${spec.label} executable, or the folder holding it.` };
|
|
66
|
+
if (!path.isAbsolute(toolPath))
|
|
67
|
+
return { ok: false, error: `toolPath must be absolute ("${toolPath}" is relative): the config is read from every session, whatever its working directory.` };
|
|
68
|
+
const resolved = await resolveToolPath(spec, toolPath);
|
|
69
|
+
if (!resolved) {
|
|
70
|
+
return { ok: false, error: `No ${spec.executables.join(" / ")} at ${toolPath}: give the executable itself, or the folder that directly holds it.` };
|
|
71
|
+
}
|
|
72
|
+
const entry = { path: resolved, installedBy: "user", rememberedAt: (options.now ?? (() => new Date()))().toISOString() };
|
|
73
|
+
const config = { ...loaded.config, tools: { ...loaded.config.tools, [spec.id]: entry } };
|
|
74
|
+
return {
|
|
75
|
+
ok: true,
|
|
76
|
+
action,
|
|
77
|
+
tool: spec.id,
|
|
78
|
+
file,
|
|
79
|
+
plan: userConfigWritePlan(config, `remember ${spec.label} at ${resolved} in the per-user config`, file),
|
|
80
|
+
config,
|
|
81
|
+
entry,
|
|
82
|
+
...(previous ? { previous } : {}),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { promises as fs } from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { StringDecoder } from "node:string_decoder";
|
|
5
|
+
import { toolEnvironment } from "./env.js";
|
|
6
|
+
const MAX_CAPTURE_BYTES = 1_000_000;
|
|
7
|
+
const TRUNCATION_NOTE = "\n...[truncated]";
|
|
8
|
+
export function assertAbsolute(paths) {
|
|
9
|
+
const relative = paths.filter((p) => !path.isAbsolute(p));
|
|
10
|
+
if (relative.length === 0)
|
|
11
|
+
return;
|
|
12
|
+
const error = new Error(`expected absolute path(s), got: ${relative.join(", ")}`);
|
|
13
|
+
error.code = "relative-path";
|
|
14
|
+
error.hint = "divine.exe silently writes nothing and exits 0 when given a relative or malformed path; resolve to an absolute path first.";
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
export function toNativePath(p) {
|
|
18
|
+
return process.platform === "win32" ? p.replace(/\//g, "\\") : p;
|
|
19
|
+
}
|
|
20
|
+
function appendCapped(buf, chunk, truncatedRef, cap = MAX_CAPTURE_BYTES) {
|
|
21
|
+
if (truncatedRef.value)
|
|
22
|
+
return buf;
|
|
23
|
+
const combined = buf + chunk;
|
|
24
|
+
if (combined.length <= cap)
|
|
25
|
+
return combined;
|
|
26
|
+
truncatedRef.value = true;
|
|
27
|
+
return combined.slice(0, cap) + TRUNCATION_NOTE;
|
|
28
|
+
}
|
|
29
|
+
async function findMissingOutputs(expected) {
|
|
30
|
+
const missing = [];
|
|
31
|
+
for (const p of expected) {
|
|
32
|
+
try {
|
|
33
|
+
const stat = await fs.stat(p);
|
|
34
|
+
if (stat.isDirectory()) {
|
|
35
|
+
const entries = await fs.readdir(p);
|
|
36
|
+
if (entries.length === 0)
|
|
37
|
+
missing.push(p);
|
|
38
|
+
}
|
|
39
|
+
else if (!stat.isFile() || stat.size === 0) {
|
|
40
|
+
missing.push(p);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
missing.push(p);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return missing;
|
|
48
|
+
}
|
|
49
|
+
async function snapshotOutputs(expected) {
|
|
50
|
+
const before = new Map();
|
|
51
|
+
for (const p of expected) {
|
|
52
|
+
try {
|
|
53
|
+
const stat = await fs.stat(p);
|
|
54
|
+
if (stat.isFile())
|
|
55
|
+
before.set(p, { size: stat.size, mtimeMs: stat.mtimeMs });
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return before;
|
|
61
|
+
}
|
|
62
|
+
async function findStaleOutputs(before) {
|
|
63
|
+
const stale = [];
|
|
64
|
+
for (const [p, prev] of before) {
|
|
65
|
+
try {
|
|
66
|
+
const stat = await fs.stat(p);
|
|
67
|
+
if (stat.isFile() && stat.size === prev.size && stat.mtimeMs === prev.mtimeMs)
|
|
68
|
+
stale.push(p);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return stale;
|
|
74
|
+
}
|
|
75
|
+
const CMD_UNSAFE = /["%!\r\n]/;
|
|
76
|
+
export function batchFileArgv(file, args, env = process.env) {
|
|
77
|
+
for (const token of [file, ...args]) {
|
|
78
|
+
if (CMD_UNSAFE.test(token)) {
|
|
79
|
+
throw new Error(`cannot pass ${JSON.stringify(token)} to a batch file safely: it contains a character cmd.exe reinterprets (" % ! or a line break)`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const line = [file, ...args].map((t) => `"${t}"`).join(" ");
|
|
83
|
+
const comspec = Object.entries(env).find(([k, v]) => k.toLowerCase() === "comspec" && v)?.[1];
|
|
84
|
+
return { command: comspec ?? "cmd.exe", argv: ["/d", "/s", "/c", `"${line}"`] };
|
|
85
|
+
}
|
|
86
|
+
export function isBatchFile(p) {
|
|
87
|
+
return /\.(bat|cmd)$/i.test(p);
|
|
88
|
+
}
|
|
89
|
+
function killTree(child) {
|
|
90
|
+
if (process.platform === "win32" && child.pid !== undefined) {
|
|
91
|
+
try {
|
|
92
|
+
spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore", windowsHide: true }).on("error", () => child.kill());
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
child.kill();
|
|
99
|
+
}
|
|
100
|
+
export async function runTool(location, options) {
|
|
101
|
+
const command = location.path ?? location.label;
|
|
102
|
+
const args = options.args;
|
|
103
|
+
const started = Date.now();
|
|
104
|
+
if (!location.found || !location.path) {
|
|
105
|
+
return {
|
|
106
|
+
tool: location.id,
|
|
107
|
+
command,
|
|
108
|
+
args,
|
|
109
|
+
ok: false,
|
|
110
|
+
exitCode: null,
|
|
111
|
+
stdout: "",
|
|
112
|
+
stderr: "",
|
|
113
|
+
durationMs: 0,
|
|
114
|
+
missingOutputs: [],
|
|
115
|
+
stdoutTruncated: false,
|
|
116
|
+
stderrTruncated: false,
|
|
117
|
+
error: {
|
|
118
|
+
code: "not-found",
|
|
119
|
+
message: `${location.label} is not installed or could not be located.`,
|
|
120
|
+
hint: location.hint,
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const { env, filled: envFilled } = toolEnvironment(options.env);
|
|
125
|
+
const before = await snapshotOutputs(options.expectOutputs ?? []);
|
|
126
|
+
const result = await new Promise((resolve) => {
|
|
127
|
+
let settled = false;
|
|
128
|
+
let stdout = "";
|
|
129
|
+
let stderr = "";
|
|
130
|
+
const stdoutTruncated = { value: false };
|
|
131
|
+
const stderrTruncated = { value: false };
|
|
132
|
+
let child;
|
|
133
|
+
try {
|
|
134
|
+
const [exe, ...prefix] = location.interpreter?.length
|
|
135
|
+
? [...location.interpreter, location.path]
|
|
136
|
+
: [location.path];
|
|
137
|
+
const spawnOptions = {
|
|
138
|
+
cwd: options.cwd,
|
|
139
|
+
env,
|
|
140
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
141
|
+
};
|
|
142
|
+
if (process.platform === "win32" && isBatchFile(exe)) {
|
|
143
|
+
const shim = batchFileArgv(exe, [...prefix, ...args], env);
|
|
144
|
+
child = spawn(shim.command, shim.argv, { ...spawnOptions, windowsVerbatimArguments: true });
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
child = spawn(exe, [...prefix, ...args], spawnOptions);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
resolve(spawnFailed(location, command, args, started, err));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const timer = options.timeoutMs
|
|
155
|
+
? setTimeout(() => {
|
|
156
|
+
if (settled)
|
|
157
|
+
return;
|
|
158
|
+
settled = true;
|
|
159
|
+
killTree(child);
|
|
160
|
+
resolve({
|
|
161
|
+
tool: location.id,
|
|
162
|
+
command,
|
|
163
|
+
args,
|
|
164
|
+
ok: false,
|
|
165
|
+
exitCode: null,
|
|
166
|
+
stdout,
|
|
167
|
+
stderr,
|
|
168
|
+
durationMs: Date.now() - started,
|
|
169
|
+
missingOutputs: [],
|
|
170
|
+
stdoutTruncated: stdoutTruncated.value,
|
|
171
|
+
stderrTruncated: stderrTruncated.value,
|
|
172
|
+
error: {
|
|
173
|
+
code: "timeout",
|
|
174
|
+
message: `${location.label} did not finish within ${options.timeoutMs}ms.`,
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
}, options.timeoutMs)
|
|
178
|
+
: undefined;
|
|
179
|
+
const cap = options.maxCaptureBytes ?? MAX_CAPTURE_BYTES;
|
|
180
|
+
const stdoutDecoder = new StringDecoder("utf8");
|
|
181
|
+
const stderrDecoder = new StringDecoder("utf8");
|
|
182
|
+
child.stdout?.on("data", (chunk) => {
|
|
183
|
+
stdout = appendCapped(stdout, stdoutDecoder.write(chunk), stdoutTruncated, cap);
|
|
184
|
+
});
|
|
185
|
+
child.stderr?.on("data", (chunk) => {
|
|
186
|
+
stderr = appendCapped(stderr, stderrDecoder.write(chunk), stderrTruncated, cap);
|
|
187
|
+
});
|
|
188
|
+
child.on("error", (err) => {
|
|
189
|
+
if (settled)
|
|
190
|
+
return;
|
|
191
|
+
settled = true;
|
|
192
|
+
if (timer)
|
|
193
|
+
clearTimeout(timer);
|
|
194
|
+
resolve(spawnFailed(location, command, args, started, err));
|
|
195
|
+
});
|
|
196
|
+
child.on("close", (exitCode) => {
|
|
197
|
+
if (settled)
|
|
198
|
+
return;
|
|
199
|
+
settled = true;
|
|
200
|
+
if (timer)
|
|
201
|
+
clearTimeout(timer);
|
|
202
|
+
resolve(finish({
|
|
203
|
+
location,
|
|
204
|
+
command,
|
|
205
|
+
args,
|
|
206
|
+
started,
|
|
207
|
+
exitCode,
|
|
208
|
+
stdout,
|
|
209
|
+
stderr,
|
|
210
|
+
stdoutTruncated: stdoutTruncated.value,
|
|
211
|
+
stderrTruncated: stderrTruncated.value,
|
|
212
|
+
expectOutputs: options.expectOutputs ?? [],
|
|
213
|
+
before,
|
|
214
|
+
requireFreshOutputs: options.requireFreshOutputs ?? false,
|
|
215
|
+
}));
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
return envFilled.length > 0 ? { ...result, envFilled } : result;
|
|
219
|
+
}
|
|
220
|
+
function spawnFailed(location, command, args, started, err) {
|
|
221
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
222
|
+
return {
|
|
223
|
+
tool: location.id,
|
|
224
|
+
command,
|
|
225
|
+
args,
|
|
226
|
+
ok: false,
|
|
227
|
+
exitCode: null,
|
|
228
|
+
stdout: "",
|
|
229
|
+
stderr: "",
|
|
230
|
+
durationMs: Date.now() - started,
|
|
231
|
+
missingOutputs: [],
|
|
232
|
+
stdoutTruncated: false,
|
|
233
|
+
stderrTruncated: false,
|
|
234
|
+
error: {
|
|
235
|
+
code: "spawn-failed",
|
|
236
|
+
message: `Failed to start ${location.label}: ${message}`,
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
async function finish(a) {
|
|
241
|
+
const durationMs = Date.now() - a.started;
|
|
242
|
+
const missingOutputs = await findMissingOutputs(a.expectOutputs);
|
|
243
|
+
const staleOutputs = await findStaleOutputs(a.before);
|
|
244
|
+
if (a.exitCode !== 0) {
|
|
245
|
+
return {
|
|
246
|
+
tool: a.location.id,
|
|
247
|
+
command: a.command,
|
|
248
|
+
args: a.args,
|
|
249
|
+
ok: false,
|
|
250
|
+
exitCode: a.exitCode,
|
|
251
|
+
stdout: a.stdout,
|
|
252
|
+
stderr: a.stderr,
|
|
253
|
+
durationMs,
|
|
254
|
+
missingOutputs,
|
|
255
|
+
stdoutTruncated: a.stdoutTruncated,
|
|
256
|
+
stderrTruncated: a.stderrTruncated,
|
|
257
|
+
error: {
|
|
258
|
+
code: "non-zero-exit",
|
|
259
|
+
message: `${a.location.label} exited with code ${a.exitCode}.`,
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
if (a.requireFreshOutputs && staleOutputs.length > 0) {
|
|
264
|
+
return {
|
|
265
|
+
tool: a.location.id,
|
|
266
|
+
command: a.command,
|
|
267
|
+
args: a.args,
|
|
268
|
+
ok: false,
|
|
269
|
+
exitCode: a.exitCode,
|
|
270
|
+
stdout: a.stdout,
|
|
271
|
+
stderr: a.stderr,
|
|
272
|
+
durationMs,
|
|
273
|
+
missingOutputs: staleOutputs,
|
|
274
|
+
staleOutputs,
|
|
275
|
+
stdoutTruncated: a.stdoutTruncated,
|
|
276
|
+
stderrTruncated: a.stderrTruncated,
|
|
277
|
+
error: {
|
|
278
|
+
code: "no-output",
|
|
279
|
+
message: `${a.location.label} exited 0 but did not rewrite: ${staleOutputs.join(", ")} (each is unchanged from before the run, so it is left over from an earlier one).`,
|
|
280
|
+
hint: "WolvenKit names a packed archive after its input folder, and some commands exit 0 without writing; check that the tool's own output name matches the one the step expects.",
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
if (missingOutputs.length > 0) {
|
|
285
|
+
return {
|
|
286
|
+
tool: a.location.id,
|
|
287
|
+
command: a.command,
|
|
288
|
+
args: a.args,
|
|
289
|
+
ok: false,
|
|
290
|
+
exitCode: a.exitCode,
|
|
291
|
+
stdout: a.stdout,
|
|
292
|
+
stderr: a.stderr,
|
|
293
|
+
durationMs,
|
|
294
|
+
missingOutputs,
|
|
295
|
+
stdoutTruncated: a.stdoutTruncated,
|
|
296
|
+
stderrTruncated: a.stderrTruncated,
|
|
297
|
+
error: {
|
|
298
|
+
code: "no-output",
|
|
299
|
+
message: `${a.location.label} exited 0 but did not produce: ${missingOutputs.join(", ")}.`,
|
|
300
|
+
hint: "divine.exe in particular is known to exit 0 and write nothing when given a malformed or relative path; double-check the source/destination arguments before assuming this run's inputs were wrong.",
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
tool: a.location.id,
|
|
306
|
+
command: a.command,
|
|
307
|
+
args: a.args,
|
|
308
|
+
ok: true,
|
|
309
|
+
exitCode: a.exitCode,
|
|
310
|
+
stdout: a.stdout,
|
|
311
|
+
stderr: a.stderr,
|
|
312
|
+
durationMs,
|
|
313
|
+
missingOutputs: [],
|
|
314
|
+
staleOutputs,
|
|
315
|
+
stdoutTruncated: a.stdoutTruncated,
|
|
316
|
+
stderrTruncated: a.stderrTruncated,
|
|
317
|
+
};
|
|
318
|
+
}
|