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,696 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { pathExists, walk } from "../fsutil.js";
|
|
4
|
+
import { globToRegExp, isExcludedByGlobs } from "../validate/index.js";
|
|
5
|
+
import { findRunningProcesses } from "../process.js";
|
|
6
|
+
import { executePlan, isIdenticalFile, planCopy, planDelete, planJunction, planMkdir, planUnlink, sha256File } from "../safety/index.js";
|
|
7
|
+
import { classifyReload } from "./reload.js";
|
|
8
|
+
import { runReloadApply } from "./reload-apply.js";
|
|
9
|
+
import { computePrune, defaultLocked, duplicateModuleUuidBlock, inspectJunctions, moduleShortDescReport, normalizeForCompare, staleArtifactBlock, waitForGameExit, } from "./guards.js";
|
|
10
|
+
import { artifactSources, buildSourceGraph } from "./sources.js";
|
|
11
|
+
const BUILD_MANIFEST_FILE = ".modwright-build-manifest.json";
|
|
12
|
+
const LOCKED_COPY_BACKOFF_MS = [200, 400, 800, 1600];
|
|
13
|
+
function execCommandLine(extras) {
|
|
14
|
+
const { command, args } = (extras ?? {});
|
|
15
|
+
if (typeof command !== "string")
|
|
16
|
+
return undefined;
|
|
17
|
+
const argv = Array.isArray(args) ? args.filter((a) => typeof a === "string") : [];
|
|
18
|
+
return [command, ...argv].join(" ");
|
|
19
|
+
}
|
|
20
|
+
async function readBuildManifest(outDir) {
|
|
21
|
+
const byOutput = new Map();
|
|
22
|
+
const outputEntries = new Map();
|
|
23
|
+
const recordedByOutput = new Map();
|
|
24
|
+
if (!outDir)
|
|
25
|
+
return { byOutput, outputEntries, recordedByOutput };
|
|
26
|
+
const file = path.join(outDir, BUILD_MANIFEST_FILE);
|
|
27
|
+
let manifest;
|
|
28
|
+
try {
|
|
29
|
+
manifest = JSON.parse(await fs.readFile(file, "utf8"));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return { byOutput, outputEntries, recordedByOutput };
|
|
33
|
+
}
|
|
34
|
+
for (const step of manifest.steps ?? []) {
|
|
35
|
+
const provenance = {
|
|
36
|
+
step: step.step,
|
|
37
|
+
id: step.id,
|
|
38
|
+
verified: step.verified,
|
|
39
|
+
command: step.verified ? undefined : execCommandLine(step.extras),
|
|
40
|
+
};
|
|
41
|
+
const recorded = { step: step.step, id: step.id, inputs: (step.inputs ?? []).map((i) => i.path) };
|
|
42
|
+
for (const output of step.outputs ?? []) {
|
|
43
|
+
byOutput.set(normalizeForCompare(output.path), provenance);
|
|
44
|
+
outputEntries.set(normalizeForCompare(output.path), output);
|
|
45
|
+
recordedByOutput.set(normalizeForCompare(output.path), recorded);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { file, manifest, byOutput, outputEntries, recordedByOutput };
|
|
49
|
+
}
|
|
50
|
+
async function builtArtifactPath(target, project, outDir) {
|
|
51
|
+
const source = project.sources.find((s) => s.id === target.from);
|
|
52
|
+
if (source)
|
|
53
|
+
return { path: source.absPath, source: source.absPath };
|
|
54
|
+
if (path.isAbsolute(target.from))
|
|
55
|
+
return { path: target.from };
|
|
56
|
+
const candidates = [...(outDir ? [path.join(outDir, target.from)] : []), path.join(project.root, target.from)];
|
|
57
|
+
for (const candidate of candidates) {
|
|
58
|
+
if (await pathExists(candidate))
|
|
59
|
+
return { path: candidate };
|
|
60
|
+
}
|
|
61
|
+
return { path: candidates[0] };
|
|
62
|
+
}
|
|
63
|
+
function toPosix(p) {
|
|
64
|
+
return p.split(path.sep).join("/");
|
|
65
|
+
}
|
|
66
|
+
async function expandTarget(fromAbs, toAbs, isDirectory, excludeGlobs, projectRoot) {
|
|
67
|
+
const patterns = excludeGlobs.map(globToRegExp);
|
|
68
|
+
const matches = (rel, abs) => isExcludedByGlobs(patterns, rel, projectRoot ? toPosix(path.relative(projectRoot, abs)) : undefined);
|
|
69
|
+
if (!isDirectory) {
|
|
70
|
+
return matches(path.basename(fromAbs), fromAbs)
|
|
71
|
+
? { copies: [], excluded: [fromAbs], links: [] }
|
|
72
|
+
: { copies: [{ from: fromAbs, to: toAbs }], excluded: [], links: [] };
|
|
73
|
+
}
|
|
74
|
+
const copies = [];
|
|
75
|
+
const excluded = [];
|
|
76
|
+
const walked = await walk(fromAbs, { limit: 200_000, skipDirs: [".modwright"], stat: false });
|
|
77
|
+
if (walked.truncated) {
|
|
78
|
+
throw new Error(`deploy: ${fromAbs} holds more than 200000 files; the listing was cut short, so nothing was planned`);
|
|
79
|
+
}
|
|
80
|
+
for (const rel of walked.files) {
|
|
81
|
+
const from = path.join(fromAbs, rel);
|
|
82
|
+
if (matches(toPosix(rel), from)) {
|
|
83
|
+
excluded.push(from);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
copies.push({ from, to: path.join(toAbs, rel) });
|
|
87
|
+
}
|
|
88
|
+
return { copies, excluded, links: (walked.links ?? []).map((rel) => path.join(fromAbs, rel)) };
|
|
89
|
+
}
|
|
90
|
+
function deferTargets(lockedTargets, running, waitMinutes, waitedMs) {
|
|
91
|
+
const holders = running.map((p) => `${p.name}${p.pid ? ` (pid ${p.pid})` : ""}`).join(", ");
|
|
92
|
+
return lockedTargets.map((t) => ({
|
|
93
|
+
rootId: t.rootId,
|
|
94
|
+
from: t.target.from,
|
|
95
|
+
to: t.toAbs,
|
|
96
|
+
processes: running,
|
|
97
|
+
message: `${holders} is running and holds ${t.toAbs}. ` +
|
|
98
|
+
(waitMinutes > 0
|
|
99
|
+
? `Waited ${Math.round(waitedMs / 1000)}s (the ${waitMinutes} minute bound) for it to exit. `
|
|
100
|
+
: `No wait was requested (waitMinutes: 0). `) +
|
|
101
|
+
`This target was deferred; everything else in the plan was deployed.`,
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
function deferralInstruction(deferred) {
|
|
105
|
+
const holders = [...new Set(deferred.flatMap((d) => d.processes.map((p) => p.name)))].join(", ");
|
|
106
|
+
return `Close ${holders || "the game"} and run deploy again for: ${deferred.map((d) => d.to).join(", ")}.`;
|
|
107
|
+
}
|
|
108
|
+
function clampWaitMinutes(value) {
|
|
109
|
+
if (value === undefined)
|
|
110
|
+
return 0;
|
|
111
|
+
return Math.min(30, Math.max(0, Math.trunc(value)));
|
|
112
|
+
}
|
|
113
|
+
export async function planDeploy(inputs) {
|
|
114
|
+
const { project, surface } = inputs;
|
|
115
|
+
const mode = inputs.mode ?? "dry-run";
|
|
116
|
+
const deployMode = inputs.deployMode ?? "pak";
|
|
117
|
+
const backupRoot = path.join(project.root, ".modwright", "backups");
|
|
118
|
+
const blocked = [];
|
|
119
|
+
const warnings = [];
|
|
120
|
+
const assumptions = [];
|
|
121
|
+
const verifiedBy = [];
|
|
122
|
+
const targets = [];
|
|
123
|
+
const skipped = [];
|
|
124
|
+
const skippedDestDirs = [];
|
|
125
|
+
const noteSkippedDir = (target) => {
|
|
126
|
+
if (!target.into)
|
|
127
|
+
return;
|
|
128
|
+
const skRoot = roots?.find((r) => r.id === target.rootId);
|
|
129
|
+
if (skRoot)
|
|
130
|
+
skippedDestDirs.push(path.join(skRoot.path, target.into));
|
|
131
|
+
};
|
|
132
|
+
const notDeployedLocked = [];
|
|
133
|
+
const excluded = [];
|
|
134
|
+
const deferred = [];
|
|
135
|
+
const variantExcludedTargets = new Set();
|
|
136
|
+
const deploy = project.project.deploy;
|
|
137
|
+
const waitMinutes = clampWaitMinutes(inputs.waitMinutes ?? deploy?.waitMinutes);
|
|
138
|
+
const emptyProcessCheck = { checked: false, running: [], lockedPaths: [], waitMinutes };
|
|
139
|
+
if (!deploy || deploy.targets.length === 0) {
|
|
140
|
+
return {
|
|
141
|
+
blocked: [
|
|
142
|
+
{
|
|
143
|
+
code: "no-deploy-config",
|
|
144
|
+
message: `${project.project.name}: modwright.json declares no deploy targets — there is nothing to deploy.`,
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
confidence: { level: "high", assumptions: [], verifiedBy: ["modwright.json"] },
|
|
148
|
+
targets: [],
|
|
149
|
+
skipped: [],
|
|
150
|
+
deferred: [],
|
|
151
|
+
junctions: [],
|
|
152
|
+
pruned: [],
|
|
153
|
+
skippedDestDirs: [],
|
|
154
|
+
excluded: [],
|
|
155
|
+
warnings,
|
|
156
|
+
writtenPaths: [],
|
|
157
|
+
processCheck: emptyProcessCheck,
|
|
158
|
+
backupRoot,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
const roots = inputs.roots ?? (inputs.install ? surface.modRoots(inputs.install) : undefined);
|
|
162
|
+
if (!roots) {
|
|
163
|
+
return {
|
|
164
|
+
blocked: [
|
|
165
|
+
{
|
|
166
|
+
code: "no-install",
|
|
167
|
+
message: `No ${surface.name} install was located, so deploy cannot resolve any root id. Pass an install path.`,
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
confidence: { level: "high", assumptions: [], verifiedBy: [] },
|
|
171
|
+
targets: [],
|
|
172
|
+
skipped: [],
|
|
173
|
+
deferred: [],
|
|
174
|
+
junctions: [],
|
|
175
|
+
pruned: [],
|
|
176
|
+
skippedDestDirs: [],
|
|
177
|
+
excluded: [],
|
|
178
|
+
warnings,
|
|
179
|
+
writtenPaths: [],
|
|
180
|
+
processCheck: emptyProcessCheck,
|
|
181
|
+
backupRoot,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
const outDir = inputs.outDir ?? project.build?.absOutDir;
|
|
185
|
+
const manifestIndex = await readBuildManifest(outDir);
|
|
186
|
+
if (manifestIndex.file) {
|
|
187
|
+
verifiedBy.push(`build manifest at ${manifestIndex.file}`);
|
|
188
|
+
}
|
|
189
|
+
else if (outDir) {
|
|
190
|
+
assumptions.push(`no build manifest at ${path.join(outDir, BUILD_MANIFEST_FILE)}: deployed artifacts were matched by file existence alone, not against what a build recorded producing.`);
|
|
191
|
+
}
|
|
192
|
+
for (const target of deploy.targets) {
|
|
193
|
+
if (target.variant && target.variant !== inputs.variant) {
|
|
194
|
+
skipped.push({
|
|
195
|
+
rootId: target.rootId,
|
|
196
|
+
from: target.from,
|
|
197
|
+
reason: `only deploys under build variant "${target.variant}"${inputs.variant ? `, not "${inputs.variant}"` : " (no variant requested)"}`,
|
|
198
|
+
});
|
|
199
|
+
noteSkippedDir(target);
|
|
200
|
+
variantExcludedTargets.add(target);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (target.mode && target.mode !== deployMode) {
|
|
204
|
+
skipped.push({ rootId: target.rootId, from: target.from, reason: `target is mode "${target.mode}", this deploy is mode "${deployMode}"` });
|
|
205
|
+
const skippedRoot = roots.find((r) => r.id === target.rootId);
|
|
206
|
+
if (skippedRoot && (target.locked ?? defaultLocked(target.rootId))) {
|
|
207
|
+
notDeployedLocked.push({ target, rootPath: skippedRoot.path });
|
|
208
|
+
}
|
|
209
|
+
noteSkippedDir(target);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
const root = roots.find((r) => r.id === target.rootId);
|
|
213
|
+
if (!root) {
|
|
214
|
+
blocked.push({
|
|
215
|
+
code: "unknown-root",
|
|
216
|
+
message: `deploy target "${target.from}" names root "${target.rootId}", which ${surface.name} does not have. Known roots: ${roots.map((r) => r.id).join(", ")}`,
|
|
217
|
+
detail: { from: target.from, rootId: target.rootId },
|
|
218
|
+
});
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
const source = project.sources.find((s) => s.id === target.from);
|
|
222
|
+
const candidates = source
|
|
223
|
+
? [source.absPath]
|
|
224
|
+
: path.isAbsolute(target.from)
|
|
225
|
+
? [target.from]
|
|
226
|
+
: [...(outDir ? [path.join(outDir, target.from)] : []), path.join(project.root, target.from)];
|
|
227
|
+
let fromAbs;
|
|
228
|
+
let stat;
|
|
229
|
+
for (const candidate of candidates) {
|
|
230
|
+
try {
|
|
231
|
+
stat = await fs.stat(candidate);
|
|
232
|
+
fromAbs = candidate;
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (!fromAbs || !stat) {
|
|
239
|
+
blocked.push({
|
|
240
|
+
code: "missing-artifact",
|
|
241
|
+
message: `deploy target "${target.from}" does not exist (looked at ${candidates.join(", ")}) — this build did not produce it. ` +
|
|
242
|
+
`Deploying an artifact the build did not produce is the stale-deploy bug; run build first.`,
|
|
243
|
+
detail: { from: target.from, rootId: target.rootId, lookedAt: candidates },
|
|
244
|
+
});
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
const provenance = manifestIndex.byOutput.get(normalizeForCompare(fromAbs));
|
|
248
|
+
const recorded = manifestIndex.outputEntries.get(normalizeForCompare(fromAbs));
|
|
249
|
+
if (provenance && recorded && !recorded.files && stat.isFile()) {
|
|
250
|
+
const current = stat.size === recorded.sizeBytes ? (await sha256File(fromAbs)).sha256 : undefined;
|
|
251
|
+
if (current !== recorded.sha256) {
|
|
252
|
+
blocked.push({
|
|
253
|
+
code: "artifact-changed",
|
|
254
|
+
message: `${fromAbs} is not the file the last successful build recorded producing (${current === undefined ? `size ${stat.size} bytes, recorded ${recorded.sizeBytes}` : "sha256 differs"}). A build that failed after writing it, or a hand edit, left it there; run build again and deploy what it verifies.`,
|
|
255
|
+
detail: { from: target.from, path: fromAbs, recordedSha256: recorded.sha256, recordedSizeBytes: recorded.sizeBytes, sizeBytes: stat.size },
|
|
256
|
+
});
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
verifiedBy.push(`sha256 of ${path.basename(fromAbs)} matches the build manifest`);
|
|
260
|
+
}
|
|
261
|
+
if (manifestIndex.file && !provenance && !source) {
|
|
262
|
+
assumptions.push(`${target.from} exists under the build outDir but is not an output of any step in the recorded build (a hand-authored sidecar, or a leftover); it is deployed on file existence alone.`);
|
|
263
|
+
}
|
|
264
|
+
if (provenance && !provenance.verified) {
|
|
265
|
+
assumptions.push(`${target.from} was produced by an unverified ${provenance.step} step${provenance.command ? `: ${provenance.command}` : ""} — ModWright checked only that it exited 0 and wrote its declared outputs.`);
|
|
266
|
+
}
|
|
267
|
+
const locked = target.locked ?? defaultLocked(target.rootId);
|
|
268
|
+
if (target.waitForGameExit !== undefined && target.waitForGameExit !== locked) {
|
|
269
|
+
warnings.push(`deploy target "${target.from}" sets waitForGameExit: ${target.waitForGameExit} but its lock class is ${locked} ` +
|
|
270
|
+
`(root "${target.rootId}"${target.locked === undefined ? ", by default" : ", set explicitly"}). The lock class is what deploy acts on; set "locked" to say otherwise.`);
|
|
271
|
+
}
|
|
272
|
+
const toAbs = target.into ? path.join(root.path, target.into) : path.join(root.path, path.basename(fromAbs));
|
|
273
|
+
const isDirectory = stat.isDirectory();
|
|
274
|
+
const excludeGlobs = [...(project.project.build?.exclude ?? []), ...(target.exclude ?? [])];
|
|
275
|
+
const { copies, excluded: targetExcluded, links } = await expandTarget(fromAbs, toAbs, isDirectory, excludeGlobs, project.root);
|
|
276
|
+
if (links.length > 0) {
|
|
277
|
+
warnings.push(`deploy target "${target.from}": ${links.length} symbolic link(s)/junction(s) under it are not followed and not deployed: ${links.slice(0, 10).join(", ")}${links.length > 10 ? ", ..." : ""}.`);
|
|
278
|
+
}
|
|
279
|
+
if (targetExcluded.length > 0) {
|
|
280
|
+
excluded.push(...targetExcluded);
|
|
281
|
+
}
|
|
282
|
+
if (copies.length === 0) {
|
|
283
|
+
const reason = targetExcluded.length > 0
|
|
284
|
+
? `nothing left to copy: every file under it matches an exclude glob [${excludeGlobs.join(", ")}]`
|
|
285
|
+
: isDirectory
|
|
286
|
+
? "the source directory is empty"
|
|
287
|
+
: "the source file is excluded";
|
|
288
|
+
skipped.push({ rootId: target.rootId, from: target.from, reason });
|
|
289
|
+
warnings.push(`deploy target "${target.from}" skipped: ${reason}.`);
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
const writes = copies.map((c) => c.to);
|
|
293
|
+
targets.push({ target, rootId: target.rootId, rootPath: root.path, fromAbs, toAbs, locked, isDirectory, copies, excluded: targetExcluded, writes, provenance, fromSource: Boolean(source) });
|
|
294
|
+
}
|
|
295
|
+
if (deployMode === "pak") {
|
|
296
|
+
for (const t of targets) {
|
|
297
|
+
const dupBlock = await duplicateModuleUuidBlock(surface, t, project.project.identity);
|
|
298
|
+
if (dupBlock)
|
|
299
|
+
blocked.push(dupBlock);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
const sourceGraph = await buildSourceGraph(project, outDir);
|
|
303
|
+
const lockedTargets = targets.filter((t) => t.locked);
|
|
304
|
+
const LOOSE_CONTEXT = "A packed mod of the same identity wins over a junctioned loose copy, so this pak would shadow the loose files.";
|
|
305
|
+
const recordedFor = (built) => manifestIndex.recordedByOutput.get(normalizeForCompare(built));
|
|
306
|
+
const stalePairs = lockedTargets.map((t) => {
|
|
307
|
+
const sources = artifactSources(sourceGraph, t.fromAbs, {
|
|
308
|
+
fromSourcePath: t.fromSource ? t.fromAbs : undefined,
|
|
309
|
+
recorded: recordedFor(t.fromAbs),
|
|
310
|
+
});
|
|
311
|
+
return deployMode === "loose"
|
|
312
|
+
? { artifact: t.toAbs, context: LOOSE_CONTEXT, kind: "installed", sources }
|
|
313
|
+
: { artifact: t.fromAbs, context: "Deploying it would put older content live.", kind: "built", sources };
|
|
314
|
+
});
|
|
315
|
+
if (deployMode === "loose") {
|
|
316
|
+
for (const { target, rootPath } of notDeployedLocked) {
|
|
317
|
+
const built = await builtArtifactPath(target, project, outDir);
|
|
318
|
+
stalePairs.push({
|
|
319
|
+
artifact: path.join(rootPath, target.into ?? path.basename(target.from)),
|
|
320
|
+
context: LOOSE_CONTEXT,
|
|
321
|
+
kind: "installed",
|
|
322
|
+
sources: artifactSources(sourceGraph, built.path, { fromSourcePath: built.source, recorded: recordedFor(built.path) }),
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
let compared = 0;
|
|
327
|
+
for (const pair of stalePairs) {
|
|
328
|
+
if (!pair.sources.known) {
|
|
329
|
+
assumptions.push(`stale-artifact guard: ${pair.artifact} was not compared — ${pair.sources.via}. ` +
|
|
330
|
+
`Give the step that builds it an "inputs" list (exec) to have it checked.`);
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
compared++;
|
|
334
|
+
const block = await staleArtifactBlock(pair.artifact, pair.sources.paths, pair.context, pair.kind, pair.sources.via);
|
|
335
|
+
if (block)
|
|
336
|
+
blocked.push(block);
|
|
337
|
+
}
|
|
338
|
+
if (compared > 0) {
|
|
339
|
+
verifiedBy.push(`stale-artifact guard over ${compared} locked artifact(s), each against the sources of the build step that produces it`);
|
|
340
|
+
}
|
|
341
|
+
const { statuses: junctions, blocks: junctionBlocks } = await inspectJunctions(project.project, project.root, roots);
|
|
342
|
+
blocked.push(...junctionBlocks);
|
|
343
|
+
const unlinkOps = [];
|
|
344
|
+
const junctionOps = [];
|
|
345
|
+
let moduleShortDesc;
|
|
346
|
+
if (junctions.length > 0) {
|
|
347
|
+
verifiedBy.push(`lstat of ${junctions.length} configured junction path(s)`);
|
|
348
|
+
if (deployMode === "pak") {
|
|
349
|
+
const live = junctions.filter((j) => j.before === "link");
|
|
350
|
+
if (live.length > 0 && !inputs.unlink) {
|
|
351
|
+
blocked.push({
|
|
352
|
+
code: "junction-coexistence",
|
|
353
|
+
message: `--mode pak refuses to run while a junction to this project exists: ${live.map((j) => j.linkPath).join(", ")}. ` +
|
|
354
|
+
`A pak and a junction of the same identity both present means the pak silently wins over your loose edits. ` +
|
|
355
|
+
`Re-run with unlink: true to remove the link(s) first (the link is removed, never what it points at).`,
|
|
356
|
+
detail: { links: live.map((j) => ({ linkPath: j.linkPath, pointsAt: j.linkTargetBefore })) },
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
else if (live.length > 0) {
|
|
360
|
+
for (const j of live) {
|
|
361
|
+
unlinkOps.push(planUnlink(j.linkPath));
|
|
362
|
+
j.action = "unlink";
|
|
363
|
+
j.after = "absent";
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
for (const j of junctions) {
|
|
369
|
+
if (j.before === "directory" || j.before === "file") {
|
|
370
|
+
warnings.push(`${j.linkPath} is a real ${j.before}, not a link. It will be backed up and replaced by a link to ${j.target}.`);
|
|
371
|
+
}
|
|
372
|
+
unlinkOps.push(planUnlink(j.linkPath));
|
|
373
|
+
junctionOps.push(planJunction(j.linkPath, j.target));
|
|
374
|
+
j.action = "relink";
|
|
375
|
+
j.after = "link";
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (deployMode === "loose" && surface.loadOrderFile) {
|
|
380
|
+
moduleShortDesc = await moduleShortDescReport(project.project, surface, inputs.install, targets);
|
|
381
|
+
if (moduleShortDesc.managerWarning)
|
|
382
|
+
warnings.push(moduleShortDesc.managerWarning);
|
|
383
|
+
}
|
|
384
|
+
const processNames = surface.processNames ?? [];
|
|
385
|
+
const unchangedLocked = new Set();
|
|
386
|
+
for (const t of lockedTargets) {
|
|
387
|
+
if (t.copies.length === 0)
|
|
388
|
+
continue;
|
|
389
|
+
let same = true;
|
|
390
|
+
for (const c of t.copies) {
|
|
391
|
+
if (!(await isIdenticalFile(c.from, c.to))) {
|
|
392
|
+
same = false;
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if (same)
|
|
397
|
+
unchangedLocked.add(t);
|
|
398
|
+
}
|
|
399
|
+
const changedLocked = lockedTargets.filter((t) => !unchangedLocked.has(t));
|
|
400
|
+
const lockedPaths = changedLocked.map((t) => t.toAbs);
|
|
401
|
+
const processCheck = { checked: false, running: [], lockedPaths, waitMinutes };
|
|
402
|
+
if (processNames.length === 0) {
|
|
403
|
+
warnings.push(`${surface.name} declares no processNames, so deploy cannot tell whether the game is running.`);
|
|
404
|
+
}
|
|
405
|
+
else if (mode === "dry-run") {
|
|
406
|
+
const probe = await (inputs.findProcesses ?? findRunningProcesses)(processNames);
|
|
407
|
+
processCheck.checked = probe.checked;
|
|
408
|
+
processCheck.running = probe.running;
|
|
409
|
+
if (!probe.checked) {
|
|
410
|
+
warnings.push("could not check whether the game is running; a failed probe is not proof that it is stopped.");
|
|
411
|
+
}
|
|
412
|
+
else if (probe.running.length > 0 && lockedPaths.length > 0) {
|
|
413
|
+
processCheck.wouldWaitFor = `${probe.running.map((p) => `${p.name}${p.pid ? ` (pid ${p.pid})` : ""}`).join(", ")} holding ${lockedPaths.join(", ")}`;
|
|
414
|
+
warnings.push(`the game is running (${processCheck.wouldWaitFor}); an apply would wait up to ${waitMinutes} minute(s) for it to exit, then deploy everything else and defer the locked target(s).`);
|
|
415
|
+
deferred.push(...deferTargets(changedLocked, probe.running, waitMinutes, 0));
|
|
416
|
+
}
|
|
417
|
+
verifiedBy.push(`process check for [${processNames.join(", ")}]`);
|
|
418
|
+
}
|
|
419
|
+
else if (lockedPaths.length === 0) {
|
|
420
|
+
const probe = await (inputs.findProcesses ?? findRunningProcesses)(processNames);
|
|
421
|
+
processCheck.checked = probe.checked;
|
|
422
|
+
processCheck.running = probe.running;
|
|
423
|
+
if (probe.running.length > 0) {
|
|
424
|
+
warnings.push(`the game is running (${probe.running.map((p) => p.name).join(", ")}), but no target in this deploy is in a locked class, so the copies proceed. Deployed content still needs the reload step below to take effect.`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
const wait = await waitForGameExit(processNames, waitMinutes, inputs);
|
|
429
|
+
processCheck.checked = wait.checked;
|
|
430
|
+
processCheck.running = wait.running;
|
|
431
|
+
processCheck.waitedMs = wait.waitedMs;
|
|
432
|
+
if (!wait.checked) {
|
|
433
|
+
warnings.push("could not check whether the game is running; a failed probe is not proof that it is stopped, so the locked copies below may fail.");
|
|
434
|
+
}
|
|
435
|
+
else if (!wait.cleared) {
|
|
436
|
+
deferred.push(...deferTargets(changedLocked, wait.running, waitMinutes, wait.waitedMs));
|
|
437
|
+
warnings.push(deferralInstruction(deferred));
|
|
438
|
+
}
|
|
439
|
+
verifiedBy.push(`process check for [${processNames.join(", ")}]`);
|
|
440
|
+
}
|
|
441
|
+
const writeSet = new Set(targets.flatMap((t) => t.writes).map(normalizeForCompare));
|
|
442
|
+
const pruned = [];
|
|
443
|
+
for (const target of deploy.targets) {
|
|
444
|
+
if (!target.prune || target.prune.length === 0)
|
|
445
|
+
continue;
|
|
446
|
+
if (variantExcludedTargets.has(target))
|
|
447
|
+
continue;
|
|
448
|
+
const root = roots.find((r) => r.id === target.rootId);
|
|
449
|
+
if (!root || !(await pathExists(root.path)))
|
|
450
|
+
continue;
|
|
451
|
+
for (const leftover of await computePrune(root.path, target.prune, writeSet)) {
|
|
452
|
+
if (!pruned.includes(leftover))
|
|
453
|
+
pruned.push(leftover);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
const deferredDests = new Set(deferred.map((d) => normalizeForCompare(d.to)));
|
|
457
|
+
const toWrite = targets.filter((t) => !deferredDests.has(normalizeForCompare(t.toAbs)));
|
|
458
|
+
const mkdirs = new Set();
|
|
459
|
+
for (const t of toWrite) {
|
|
460
|
+
for (const copy of t.copies)
|
|
461
|
+
mkdirs.add(path.dirname(copy.to));
|
|
462
|
+
}
|
|
463
|
+
const operations = [
|
|
464
|
+
...unlinkOps,
|
|
465
|
+
...junctionOps,
|
|
466
|
+
...pruned.map((p) => planDelete(p)),
|
|
467
|
+
...[...mkdirs].map((d) => planMkdir(d)),
|
|
468
|
+
...toWrite.filter((t) => !t.locked).flatMap((t) => t.copies.map((c) => planCopy(c.from, c.to))),
|
|
469
|
+
...toWrite.filter((t) => t.locked).flatMap((t) => t.copies.map((c) => planCopy(c.from, c.to))),
|
|
470
|
+
];
|
|
471
|
+
const confidence = {
|
|
472
|
+
level: assumptions.length > 0 ? "medium" : "high",
|
|
473
|
+
assumptions,
|
|
474
|
+
verifiedBy,
|
|
475
|
+
};
|
|
476
|
+
const plan = blocked.length > 0
|
|
477
|
+
? undefined
|
|
478
|
+
: { purpose: `deploy ${project.project.name} (${deployMode})`, operations, backupRoot };
|
|
479
|
+
return {
|
|
480
|
+
plan,
|
|
481
|
+
blocked,
|
|
482
|
+
confidence,
|
|
483
|
+
targets,
|
|
484
|
+
skipped,
|
|
485
|
+
deferred,
|
|
486
|
+
junctions,
|
|
487
|
+
pruned,
|
|
488
|
+
skippedDestDirs,
|
|
489
|
+
excluded,
|
|
490
|
+
warnings,
|
|
491
|
+
writtenPaths: toWrite.flatMap((t) => t.writes),
|
|
492
|
+
moduleShortDesc,
|
|
493
|
+
processCheck,
|
|
494
|
+
outDir,
|
|
495
|
+
backupRoot,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
function copyKey(from, to) {
|
|
499
|
+
return `${normalizeForCompare(from)}\0${normalizeForCompare(to)}`;
|
|
500
|
+
}
|
|
501
|
+
async function observeJunctions(planned) {
|
|
502
|
+
const out = [];
|
|
503
|
+
for (const j of planned) {
|
|
504
|
+
let after = "absent";
|
|
505
|
+
try {
|
|
506
|
+
const lst = await fs.lstat(j.linkPath);
|
|
507
|
+
after = lst.isSymbolicLink() ? "link" : lst.isDirectory() ? "directory" : "file";
|
|
508
|
+
}
|
|
509
|
+
catch {
|
|
510
|
+
after = "absent";
|
|
511
|
+
}
|
|
512
|
+
out.push({ ...j, after });
|
|
513
|
+
}
|
|
514
|
+
return out;
|
|
515
|
+
}
|
|
516
|
+
export async function runDeploy(inputs) {
|
|
517
|
+
const mode = inputs.mode ?? "dry-run";
|
|
518
|
+
const deployMode = inputs.deployMode ?? "pak";
|
|
519
|
+
const planned = await planDeploy(inputs);
|
|
520
|
+
const baseSummary = {
|
|
521
|
+
project: inputs.project.project.name,
|
|
522
|
+
game: inputs.surface.id,
|
|
523
|
+
mode,
|
|
524
|
+
deployMode,
|
|
525
|
+
variant: inputs.variant,
|
|
526
|
+
outDir: planned.outDir,
|
|
527
|
+
skipped: planned.skipped,
|
|
528
|
+
deferred: planned.deferred,
|
|
529
|
+
partial: planned.deferred.length > 0,
|
|
530
|
+
excluded: planned.excluded,
|
|
531
|
+
junctions: planned.junctions,
|
|
532
|
+
moduleShortDesc: planned.moduleShortDesc,
|
|
533
|
+
processCheck: planned.processCheck,
|
|
534
|
+
};
|
|
535
|
+
if (!planned.plan) {
|
|
536
|
+
const summary = {
|
|
537
|
+
...baseSummary,
|
|
538
|
+
written: [],
|
|
539
|
+
pruned: [],
|
|
540
|
+
cleanedEmptyDirs: [],
|
|
541
|
+
warnings: planned.warnings,
|
|
542
|
+
blocked: planned.blocked,
|
|
543
|
+
reload: classifyReload(inputs.surface.id, inputs.project.project, [], { compat: inputs.compat, redHotTools: inputs.redHotTools, scriptDiffs: inputs.scriptDiffs }),
|
|
544
|
+
next: `Refused before anything was touched: ${planned.blocked.map((b) => b.message).join(" ")}`,
|
|
545
|
+
};
|
|
546
|
+
return { applied: false, partial: false, blocked: planned.blocked, confidence: planned.confidence, summary };
|
|
547
|
+
}
|
|
548
|
+
const deferredDests = new Set(planned.deferred.map((d) => normalizeForCompare(d.to)));
|
|
549
|
+
const lockedDests = new Set(planned.targets.filter((t) => t.locked).flatMap((t) => t.copies.map((c) => normalizeForCompare(c.to))));
|
|
550
|
+
const warnings = [...planned.warnings];
|
|
551
|
+
const report = await executePlan(planned.plan, {
|
|
552
|
+
mode,
|
|
553
|
+
confidence: planned.confidence,
|
|
554
|
+
now: inputs.now,
|
|
555
|
+
retry: {
|
|
556
|
+
shouldRetry: (op) => op.kind === "copy" && lockedDests.has(normalizeForCompare(op.to)),
|
|
557
|
+
delaysMs: LOCKED_COPY_BACKOFF_MS,
|
|
558
|
+
sleep: inputs.sleep,
|
|
559
|
+
onRetry: (op, attempt, error) => {
|
|
560
|
+
warnings.push(`locked copy to ${op.kind === "copy" ? op.to : ""} failed (${error instanceof Error ? error.message : String(error)}); retry ${attempt} of ${LOCKED_COPY_BACKOFF_MS.length}.`);
|
|
561
|
+
},
|
|
562
|
+
},
|
|
563
|
+
deferOnLock: (op) => op.kind === "delete",
|
|
564
|
+
});
|
|
565
|
+
const deferredPrune = report.operations.filter((r) => r.op.kind === "delete" && r.status === "deferred");
|
|
566
|
+
if (deferredPrune.length > 0) {
|
|
567
|
+
warnings.push(`${deferredPrune.length} prune target(s) were left in place because something holds them open (the game, ` +
|
|
568
|
+
`if it is running): ${deferredPrune.map((r) => r.op.path).join(", ")}. These are runtime state, not stale ` +
|
|
569
|
+
`content, so the rest of the deploy still ran; run deploy again once the holder is closed to clear them.`);
|
|
570
|
+
}
|
|
571
|
+
const copyStatus = new Map();
|
|
572
|
+
for (const r of report.operations) {
|
|
573
|
+
if (r.op.kind === "copy")
|
|
574
|
+
copyStatus.set(copyKey(r.op.from, r.op.to), r.status);
|
|
575
|
+
}
|
|
576
|
+
const statusesOf = (t) => t.copies.map((c) => copyStatus.get(copyKey(c.from, c.to)) ?? "skipped");
|
|
577
|
+
const statusOf = (t) => {
|
|
578
|
+
const statuses = statusesOf(t);
|
|
579
|
+
if (statuses.length === 0)
|
|
580
|
+
return "skipped";
|
|
581
|
+
if (statuses.some((st) => st === "failed"))
|
|
582
|
+
return "failed";
|
|
583
|
+
if (statuses.every((st) => st === "planned"))
|
|
584
|
+
return "planned";
|
|
585
|
+
if (statuses.every((st) => st === "applied" || st === "skipped") && statuses.some((st) => st === "applied"))
|
|
586
|
+
return "applied";
|
|
587
|
+
return "skipped";
|
|
588
|
+
};
|
|
589
|
+
const written = planned.targets.map((t) => ({
|
|
590
|
+
rootId: t.rootId,
|
|
591
|
+
from: t.target.from,
|
|
592
|
+
to: t.toAbs,
|
|
593
|
+
locked: t.locked,
|
|
594
|
+
status: deferredDests.has(normalizeForCompare(t.toAbs)) ? "deferred" : statusOf(t),
|
|
595
|
+
}));
|
|
596
|
+
const blocked = [...planned.blocked];
|
|
597
|
+
const failedLocked = planned.targets.filter((t) => t.locked && statusOf(t) === "failed");
|
|
598
|
+
if (failedLocked.length > 0) {
|
|
599
|
+
const probe = await (inputs.findProcesses ?? findRunningProcesses)(inputs.surface.processNames ?? []);
|
|
600
|
+
const holders = probe.running.map((p) => `${p.name}${p.pid ? ` (pid ${p.pid})` : ""}`).join(", ");
|
|
601
|
+
blocked.push({
|
|
602
|
+
code: "locked-copy-failed",
|
|
603
|
+
message: `${failedLocked.map((t) => t.toAbs).join(", ")} could not be written after ${LOCKED_COPY_BACKOFF_MS.length + 1} attempts. ` +
|
|
604
|
+
(probe.checked
|
|
605
|
+
? holders
|
|
606
|
+
? `${holders} is running and is the likely holder.`
|
|
607
|
+
: `No game process is running, so something else holds the file (antivirus, an indexer, an open editor).`
|
|
608
|
+
: `The process check could not run, so the holder is unknown.`),
|
|
609
|
+
detail: { paths: failedLocked.map((t) => t.toAbs), processes: probe.running, processCheckWorked: probe.checked },
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
const appliedWrites = mode === "apply" ? written.filter((w) => w.status === "applied") : written;
|
|
613
|
+
const writtenPaths = mode === "apply"
|
|
614
|
+
? planned.targets.flatMap((t) => t.copies.filter((c) => copyStatus.get(copyKey(c.from, c.to)) === "applied").map((c) => c.to))
|
|
615
|
+
: planned.writtenPaths;
|
|
616
|
+
const appliedDeletes = new Set(report.operations.filter((r) => r.op.kind === "delete" && r.status === "applied").map((r) => normalizeForCompare(r.op.path)));
|
|
617
|
+
const prunedApplied = mode === "apply" ? planned.pruned.filter((p) => appliedDeletes.has(normalizeForCompare(p))) : planned.pruned;
|
|
618
|
+
const junctions = mode === "apply" ? await observeJunctions(planned.junctions) : planned.junctions;
|
|
619
|
+
const reload = classifyReload(inputs.surface.id, inputs.project.project, writtenPaths, {
|
|
620
|
+
compat: inputs.compat,
|
|
621
|
+
redHotTools: inputs.redHotTools,
|
|
622
|
+
scriptDiffs: inputs.scriptDiffs,
|
|
623
|
+
});
|
|
624
|
+
let reloadApply;
|
|
625
|
+
if (inputs.reloadApply && inputs.install && inputs.surface.id === "cyberpunk2077") {
|
|
626
|
+
const reloadTargets = planned.targets.flatMap((t) => t.copies
|
|
627
|
+
.filter((c) => deferredDests.has(normalizeForCompare(t.toAbs)) || copyStatus.get(copyKey(c.from, c.to)) !== "skipped")
|
|
628
|
+
.map((c) => ({ from: c.from, to: c.to })));
|
|
629
|
+
reloadApply = await runReloadApply({
|
|
630
|
+
game: inputs.surface.id,
|
|
631
|
+
install: inputs.install,
|
|
632
|
+
targets: reloadTargets,
|
|
633
|
+
grant: inputs.reloadApply.grant,
|
|
634
|
+
allowMutate: inputs.reloadApply.allowMutate,
|
|
635
|
+
meshArchives: inputs.reloadApply.meshArchives,
|
|
636
|
+
devModule: inputs.reloadApply.devModule,
|
|
637
|
+
hotAvailable: inputs.redHotTools?.hotAvailable,
|
|
638
|
+
surface: inputs.surface,
|
|
639
|
+
projectRoot: inputs.project.root,
|
|
640
|
+
mode,
|
|
641
|
+
now: inputs.now,
|
|
642
|
+
sleep: inputs.sleep,
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
const cleanedEmptyDirs = [];
|
|
646
|
+
for (const dir of planned.skippedDestDirs) {
|
|
647
|
+
let entries;
|
|
648
|
+
try {
|
|
649
|
+
entries = await fs.readdir(dir);
|
|
650
|
+
}
|
|
651
|
+
catch {
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
if (mode === "apply") {
|
|
655
|
+
if (entries.length === 0) {
|
|
656
|
+
try {
|
|
657
|
+
await fs.rmdir(dir);
|
|
658
|
+
cleanedEmptyDirs.push(dir);
|
|
659
|
+
}
|
|
660
|
+
catch {
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
else {
|
|
665
|
+
const remaining = entries
|
|
666
|
+
.map((e) => path.join(dir, e))
|
|
667
|
+
.filter((p) => !planned.pruned.some((pr) => normalizeForCompare(pr) === normalizeForCompare(p)));
|
|
668
|
+
if (remaining.length === 0)
|
|
669
|
+
cleanedEmptyDirs.push(dir);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
const partial = planned.deferred.length > 0 || deferredPrune.length > 0;
|
|
673
|
+
const deferralNote = planned.deferred.length > 0 ? ` ${deferralInstruction(planned.deferred)}` : "";
|
|
674
|
+
const next = mode === "dry-run"
|
|
675
|
+
? `Nothing was changed. Re-run with { mode: "apply" } to write these changes; any overwrite or delete is backed up first.${deferralNote}`
|
|
676
|
+
: blocked.length > 0
|
|
677
|
+
? `Deploy did not complete. ${blocked.map((b) => b.message).join(" ")}${report.backupDir ? ` Undo what did apply with restoreBackup("${report.backupDir}", { mode: "apply" }).` : ""}`
|
|
678
|
+
: `Deployed ${appliedWrites.length} target(s)` +
|
|
679
|
+
(planned.deferred.length > 0 ? `, deferred ${planned.deferred.length}` : "") +
|
|
680
|
+
(deferredPrune.length > 0 ? `, left ${deferredPrune.length} held-open prune target(s) in place` : "") +
|
|
681
|
+
`. Reload recommendation: ${reload.overall}.${deferralNote}`;
|
|
682
|
+
const summary = {
|
|
683
|
+
...baseSummary,
|
|
684
|
+
partial,
|
|
685
|
+
junctions,
|
|
686
|
+
written,
|
|
687
|
+
pruned: prunedApplied,
|
|
688
|
+
cleanedEmptyDirs,
|
|
689
|
+
warnings,
|
|
690
|
+
blocked,
|
|
691
|
+
reload,
|
|
692
|
+
...(reloadApply ? { reloadApply } : {}),
|
|
693
|
+
next,
|
|
694
|
+
};
|
|
695
|
+
return { applied: report.applied && blocked.length === 0, partial, blocked, plan: planned.plan, report, confidence: planned.confidence, summary };
|
|
696
|
+
}
|