@skaile/workspaces 5.9.0 → 5.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (316) hide show
  1. package/CHANGELOG.md +145 -0
  2. package/README.md +12 -0
  3. package/dist/{asset-feeds-XMKKUUK6.js → asset-feeds-CXGQIDFR.js} +19 -19
  4. package/dist/{asset-feeds-XMKKUUK6.js.map → asset-feeds-CXGQIDFR.js.map} +1 -1
  5. package/dist/asset-manager/catalog-deployer.js +8 -8
  6. package/dist/asset-manager/index.js +14 -14
  7. package/dist/asset-manager/installer.js +10 -10
  8. package/dist/asset-manager/renderers.js +9 -9
  9. package/dist/asset-manager/src/index.d.ts +400 -35
  10. package/dist/asset-manager/src/index.d.ts.map +1 -1
  11. package/dist/asset-manager/src/install-agent.d.ts +3 -1
  12. package/dist/asset-manager/src/install-agent.d.ts.map +1 -1
  13. package/dist/asset-manager/src/install-instances.d.ts +31 -1
  14. package/dist/asset-manager/src/install-instances.d.ts.map +1 -1
  15. package/dist/asset-manager/src/installer.d.ts +43 -3
  16. package/dist/asset-manager/src/installer.d.ts.map +1 -1
  17. package/dist/asset-manager/src/renderers.d.ts +4 -5
  18. package/dist/asset-manager/src/renderers.d.ts.map +1 -1
  19. package/dist/bridge/drivers/claude-sdk.js +11 -11
  20. package/dist/bridge/drivers/codex.js +11 -11
  21. package/dist/bridge/drivers/continue.js +10 -10
  22. package/dist/bridge/drivers/echo.js +2 -2
  23. package/dist/bridge/drivers/gemini.js +7 -7
  24. package/dist/bridge/drivers/goose.js +10 -10
  25. package/dist/bridge/drivers/omp.js +19 -3
  26. package/dist/bridge/drivers/omp.js.map +1 -1
  27. package/dist/bridge/drivers/opencode.js +10 -10
  28. package/dist/bridge/drivers/qwen.js +7 -7
  29. package/dist/bridge/index.js +12 -12
  30. package/dist/bridge/src/drivers/omp.d.ts.map +1 -1
  31. package/dist/{chunk-4MQS7CAF.js → chunk-2F5AWQX5.js} +437 -175
  32. package/dist/chunk-2F5AWQX5.js.map +1 -0
  33. package/dist/{chunk-UM7RIAHO.js → chunk-3SM2DL2T.js} +8 -8
  34. package/dist/{chunk-UM7RIAHO.js.map → chunk-3SM2DL2T.js.map} +1 -1
  35. package/dist/{chunk-5NNNV76P.js → chunk-47U4GZ4M.js} +2 -2
  36. package/dist/{chunk-5NNNV76P.js.map → chunk-47U4GZ4M.js.map} +1 -1
  37. package/dist/{chunk-TTRYE6AC.js → chunk-4A4OD24G.js} +9 -9
  38. package/dist/{chunk-TTRYE6AC.js.map → chunk-4A4OD24G.js.map} +1 -1
  39. package/dist/{chunk-ZTBAYUDY.js → chunk-6O2X3GPW.js} +2 -2
  40. package/dist/{chunk-ZTBAYUDY.js.map → chunk-6O2X3GPW.js.map} +1 -1
  41. package/dist/{chunk-JAKOMFC6.js → chunk-ALEYHUC5.js} +114 -46
  42. package/dist/chunk-ALEYHUC5.js.map +1 -0
  43. package/dist/{chunk-QVNRCDTV.js → chunk-BNMZVPYA.js} +7 -7
  44. package/dist/chunk-BNMZVPYA.js.map +1 -0
  45. package/dist/{chunk-2FRUUDZY.js → chunk-CAHB6MHA.js} +5 -5
  46. package/dist/{chunk-2FRUUDZY.js.map → chunk-CAHB6MHA.js.map} +1 -1
  47. package/dist/{chunk-GKMZ6THE.js → chunk-CJERAGFY.js} +44 -33
  48. package/dist/chunk-CJERAGFY.js.map +1 -0
  49. package/dist/{chunk-CKQEIY43.js → chunk-COH6FQAG.js} +4 -4
  50. package/dist/{chunk-CKQEIY43.js.map → chunk-COH6FQAG.js.map} +1 -1
  51. package/dist/{chunk-UO7WPLDY.js → chunk-CPWLNZGJ.js} +3 -3
  52. package/dist/{chunk-UO7WPLDY.js.map → chunk-CPWLNZGJ.js.map} +1 -1
  53. package/dist/{chunk-MGGFBZWA.js → chunk-D3B36CYC.js} +4 -3
  54. package/dist/chunk-D3B36CYC.js.map +1 -0
  55. package/dist/{chunk-UMVKJM4Q.js → chunk-DALV4HHP.js} +18 -3
  56. package/dist/chunk-DALV4HHP.js.map +1 -0
  57. package/dist/{chunk-IMKSM5NL.js → chunk-DZMNFCWB.js} +1447 -199
  58. package/dist/chunk-DZMNFCWB.js.map +1 -0
  59. package/dist/{chunk-GB7WDMH2.js → chunk-EDYHEMJA.js} +4 -4
  60. package/dist/{chunk-GB7WDMH2.js.map → chunk-EDYHEMJA.js.map} +1 -1
  61. package/dist/{chunk-YSHVQGUS.js → chunk-FH7W6TJ4.js} +164 -41
  62. package/dist/chunk-FH7W6TJ4.js.map +1 -0
  63. package/dist/{chunk-W5DWWE4Y.js → chunk-G4FFOV3N.js} +4 -4
  64. package/dist/{chunk-W5DWWE4Y.js.map → chunk-G4FFOV3N.js.map} +1 -1
  65. package/dist/{chunk-2LWYNQK7.js → chunk-GCHPUTA3.js} +3 -3
  66. package/dist/{chunk-2LWYNQK7.js.map → chunk-GCHPUTA3.js.map} +1 -1
  67. package/dist/{chunk-JZ5EHBPI.js → chunk-GV4DZ6MT.js} +20 -4
  68. package/dist/chunk-GV4DZ6MT.js.map +1 -0
  69. package/dist/{chunk-S53LOR7B.js → chunk-HCEVNYSH.js} +3 -3
  70. package/dist/{chunk-S53LOR7B.js.map → chunk-HCEVNYSH.js.map} +1 -1
  71. package/dist/{chunk-EF7NPDDU.js → chunk-HZXUXHIV.js} +31 -17
  72. package/dist/chunk-HZXUXHIV.js.map +1 -0
  73. package/dist/{chunk-IR33D6EP.js → chunk-IER3KIPH.js} +5 -5
  74. package/dist/{chunk-IR33D6EP.js.map → chunk-IER3KIPH.js.map} +1 -1
  75. package/dist/{chunk-Y7QA2OLD.js → chunk-J4RBUFGM.js} +22 -20
  76. package/dist/chunk-J4RBUFGM.js.map +1 -0
  77. package/dist/{chunk-J7U3PIOE.js → chunk-M3B6EIJD.js} +4 -4
  78. package/dist/{chunk-J7U3PIOE.js.map → chunk-M3B6EIJD.js.map} +1 -1
  79. package/dist/{chunk-DNZYOKOJ.js → chunk-MZG34MBZ.js} +3 -3
  80. package/dist/{chunk-DNZYOKOJ.js.map → chunk-MZG34MBZ.js.map} +1 -1
  81. package/dist/{chunk-V2DXZYBB.js → chunk-NWDSLXOX.js} +3 -3
  82. package/dist/{chunk-V2DXZYBB.js.map → chunk-NWDSLXOX.js.map} +1 -1
  83. package/dist/{chunk-36AFURKZ.js → chunk-PBBPZYVG.js} +61 -12
  84. package/dist/chunk-PBBPZYVG.js.map +1 -0
  85. package/dist/{chunk-P67WJVP4.js → chunk-PQFHMQHQ.js} +10 -9
  86. package/dist/chunk-PQFHMQHQ.js.map +1 -0
  87. package/dist/{chunk-UAROCOXF.js → chunk-QVFAS5IO.js} +3 -3
  88. package/dist/{chunk-UAROCOXF.js.map → chunk-QVFAS5IO.js.map} +1 -1
  89. package/dist/{chunk-K3CVFRVS.js → chunk-QZNJOHN6.js} +3 -3
  90. package/dist/{chunk-K3CVFRVS.js.map → chunk-QZNJOHN6.js.map} +1 -1
  91. package/dist/{chunk-FVJK4QWM.js → chunk-R7NHYEJF.js} +4 -4
  92. package/dist/{chunk-FVJK4QWM.js.map → chunk-R7NHYEJF.js.map} +1 -1
  93. package/dist/{chunk-GXLQLPE5.js → chunk-RYYLMBA2.js} +5 -5
  94. package/dist/{chunk-GXLQLPE5.js.map → chunk-RYYLMBA2.js.map} +1 -1
  95. package/dist/{chunk-Y4N2NUZD.js → chunk-S3O3GKB7.js} +4 -4
  96. package/dist/{chunk-Y4N2NUZD.js.map → chunk-S3O3GKB7.js.map} +1 -1
  97. package/dist/{chunk-GTILM2CI.js → chunk-SBSA3IQA.js} +43 -12
  98. package/dist/chunk-SBSA3IQA.js.map +1 -0
  99. package/dist/{chunk-4PTSR4UU.js → chunk-T42WRQ4O.js} +3 -3
  100. package/dist/{chunk-4PTSR4UU.js.map → chunk-T42WRQ4O.js.map} +1 -1
  101. package/dist/{chunk-MR5HBOUF.js → chunk-T7CTHHG7.js} +3 -3
  102. package/dist/{chunk-MR5HBOUF.js.map → chunk-T7CTHHG7.js.map} +1 -1
  103. package/dist/{chunk-FZNG4V4J.js → chunk-TCHRMVTO.js} +4 -4
  104. package/dist/{chunk-FZNG4V4J.js.map → chunk-TCHRMVTO.js.map} +1 -1
  105. package/dist/{chunk-LAMCXQGA.js → chunk-VLTQGE2E.js} +75 -25
  106. package/dist/chunk-VLTQGE2E.js.map +1 -0
  107. package/dist/{chunk-7AI3Q5AQ.js → chunk-WSBKB4CJ.js} +71 -24
  108. package/dist/chunk-WSBKB4CJ.js.map +1 -0
  109. package/dist/{chunk-CGT45WN7.js → chunk-X77SUM4P.js} +3 -3
  110. package/dist/{chunk-CGT45WN7.js.map → chunk-X77SUM4P.js.map} +1 -1
  111. package/dist/{chunk-SCOT677Q.js → chunk-XDKH7XSA.js} +5 -5
  112. package/dist/{chunk-SCOT677Q.js.map → chunk-XDKH7XSA.js.map} +1 -1
  113. package/dist/{chunk-QVEKLSEH.js → chunk-XUSALIEI.js} +6 -6
  114. package/dist/{chunk-QVEKLSEH.js.map → chunk-XUSALIEI.js.map} +1 -1
  115. package/dist/{chunk-FYXZRVID.js → chunk-Y3HTGHJC.js} +3 -3
  116. package/dist/{chunk-FYXZRVID.js.map → chunk-Y3HTGHJC.js.map} +1 -1
  117. package/dist/chunk-YOH2L76X.js +505 -0
  118. package/dist/chunk-YOH2L76X.js.map +1 -0
  119. package/dist/{chunk-B6GIW55K.js → chunk-ZCAJZ4B2.js} +7 -4
  120. package/dist/chunk-ZCAJZ4B2.js.map +1 -0
  121. package/dist/{chunk-BTE2V4IW.js → chunk-ZPWTOUTK.js} +76 -7
  122. package/dist/{chunk-BTE2V4IW.js.map → chunk-ZPWTOUTK.js.map} +1 -1
  123. package/dist/cli/index.js +1034 -794
  124. package/dist/cli/index.js.map +1 -1
  125. package/dist/cli/src/commands/asset-cmd.d.ts.map +1 -1
  126. package/dist/cli/src/commands/catalog.d.ts +8 -5
  127. package/dist/cli/src/commands/catalog.d.ts.map +1 -1
  128. package/dist/cli/src/commands/diff.d.ts +4 -2
  129. package/dist/cli/src/commands/diff.d.ts.map +1 -1
  130. package/dist/cli/src/commands/library-cmd.d.ts.map +1 -1
  131. package/dist/cli/src/commands/library-index.d.ts +34 -2
  132. package/dist/cli/src/commands/library-index.d.ts.map +1 -1
  133. package/dist/cli/src/commands/library-publish.d.ts.map +1 -1
  134. package/dist/cli/src/commands/manage.d.ts +12 -5
  135. package/dist/cli/src/commands/manage.d.ts.map +1 -1
  136. package/dist/cli/src/commands/migrate-assets.d.ts.map +1 -1
  137. package/dist/cli/src/commands/outdated.d.ts +7 -5
  138. package/dist/cli/src/commands/outdated.d.ts.map +1 -1
  139. package/dist/cli/src/commands/project.d.ts +13 -8
  140. package/dist/cli/src/commands/project.d.ts.map +1 -1
  141. package/dist/cli/src/commands/run.d.ts.map +1 -1
  142. package/dist/cli/src/commands/source.d.ts +30 -1
  143. package/dist/cli/src/commands/source.d.ts.map +1 -1
  144. package/dist/cli/src/commands/update.d.ts.map +1 -1
  145. package/dist/cli/src/commands/validate.d.ts.map +1 -1
  146. package/dist/cli/src/commands/why.d.ts +1 -0
  147. package/dist/cli/src/commands/why.d.ts.map +1 -1
  148. package/dist/cli/src/helpers.d.ts +12 -0
  149. package/dist/cli/src/helpers.d.ts.map +1 -1
  150. package/dist/cli/src/skill-checks.d.ts +57 -4
  151. package/dist/cli/src/skill-checks.d.ts.map +1 -1
  152. package/dist/connectors/config.js +5 -5
  153. package/dist/connectors/index.js +16 -16
  154. package/dist/{connectors-C7UQ73BK.js → connectors-4QWAKPHB.js} +19 -19
  155. package/dist/{connectors-C7UQ73BK.js.map → connectors-4QWAKPHB.js.map} +1 -1
  156. package/dist/core/index.js +7 -7
  157. package/dist/core/logging.js +1 -1
  158. package/dist/core/manifest.js +2 -2
  159. package/dist/core/models.js +1 -1
  160. package/dist/core/publish-manifest.js +2 -2
  161. package/dist/core/ref.js +1 -1
  162. package/dist/core/runtime-assets.js +5 -5
  163. package/dist/core/src/index.d.ts +2 -1
  164. package/dist/core/src/index.d.ts.map +1 -1
  165. package/dist/core/src/lock.d.ts +6 -2
  166. package/dist/core/src/lock.d.ts.map +1 -1
  167. package/dist/core/src/logging/sinks/sqlite-runtime.d.ts.map +1 -1
  168. package/dist/core/src/manifest.d.ts.map +1 -1
  169. package/dist/core/src/models.d.ts +22 -6
  170. package/dist/core/src/models.d.ts.map +1 -1
  171. package/dist/core/src/publish-manifest.d.ts +29 -3
  172. package/dist/core/src/publish-manifest.d.ts.map +1 -1
  173. package/dist/core/src/repo-manager.d.ts.map +1 -1
  174. package/dist/core/src/requirements.d.ts +26 -0
  175. package/dist/core/src/requirements.d.ts.map +1 -0
  176. package/dist/core/src/walker.d.ts.map +1 -1
  177. package/dist/core/src/workspace-config.d.ts +22 -1
  178. package/dist/core/src/workspace-config.d.ts.map +1 -1
  179. package/dist/core/src/workspace-yaml-editor.d.ts +4 -1
  180. package/dist/core/src/workspace-yaml-editor.d.ts.map +1 -1
  181. package/dist/core/workspace-config.js +4 -4
  182. package/dist/deploy/index.js +7 -7
  183. package/dist/discovery/index.js +5 -5
  184. package/dist/discovery/src/builtin-providers.d.ts.map +1 -1
  185. package/dist/discovery/src/discover-manifest.d.ts.map +1 -1
  186. package/dist/discovery/src/discover.d.ts.map +1 -1
  187. package/dist/discovery/src/source-config.d.ts.map +1 -1
  188. package/dist/discovery/src/tree-entries.d.ts.map +1 -1
  189. package/dist/{ensure-sources-SRC2DVR5.js → ensure-sources-2RONZKCU.js} +17 -17
  190. package/dist/{ensure-sources-SRC2DVR5.js.map → ensure-sources-2RONZKCU.js.map} +1 -1
  191. package/dist/factory-assets/connectors/box.js +16 -16
  192. package/dist/factory-assets/connectors/deploy.js +16 -16
  193. package/dist/factory-assets/connectors/devserver.js +16 -16
  194. package/dist/factory-assets/connectors/flow/adapter.js +16 -16
  195. package/dist/factory-assets/connectors/flow/contract/normalize.d.ts.map +1 -1
  196. package/dist/factory-assets/connectors/flow/contract.js +1 -1
  197. package/dist/factory-assets/connectors/flow/engine.js +9 -9
  198. package/dist/factory-assets/connectors/flow/run-flow.js +18 -18
  199. package/dist/factory-assets/connectors/flow.js +16 -16
  200. package/dist/factory-assets/connectors/git.js +16 -16
  201. package/dist/factory-assets/connectors/gmail.js +16 -16
  202. package/dist/factory-assets/connectors/googledrive.js +16 -16
  203. package/dist/factory-assets/connectors/local.js +16 -16
  204. package/dist/factory-assets/connectors/mattermost.js +16 -16
  205. package/dist/factory-assets/connectors/memory.js +16 -16
  206. package/dist/factory-assets/connectors/minio.js +16 -16
  207. package/dist/factory-assets/connectors/postgres.js +16 -16
  208. package/dist/factory-assets/connectors/s3.js +16 -16
  209. package/dist/factory-assets/connectors/sharepoint.js +16 -16
  210. package/dist/factory-assets/connectors/sqlite.js +16 -16
  211. package/dist/factory-assets/connectors/static-server.js +16 -16
  212. package/dist/factory-assets/connectors/tunnel.js +16 -16
  213. package/dist/factory-assets/connectors/webdav.js +16 -16
  214. package/dist/factory-assets/connectors/xstate-store.js +16 -16
  215. package/dist/factory-assets/connectors/xstate.js +16 -16
  216. package/dist/factory-assets/skills/authoring-skaile-manifests/SKILL.md +9 -3
  217. package/dist/factory-assets/skills/authoring-skaile-manifests/references/dependency-standard.md +4 -2
  218. package/dist/{flows-CBYRSAGS.js → flows-EQF2CH4U.js} +12 -12
  219. package/dist/{flows-CBYRSAGS.js.map → flows-EQF2CH4U.js.map} +1 -1
  220. package/dist/{helpers-XAEKCZKT.js → helpers-CFK6YP6R.js} +17 -17
  221. package/dist/{helpers-XAEKCZKT.js.map → helpers-CFK6YP6R.js.map} +1 -1
  222. package/dist/library/index.js +11 -11
  223. package/dist/library/install.js +3 -3
  224. package/dist/library/src/index.d.ts +2 -1
  225. package/dist/library/src/index.d.ts.map +1 -1
  226. package/dist/library/src/local/library-dir.d.ts +68 -0
  227. package/dist/library/src/local/library-dir.d.ts.map +1 -0
  228. package/dist/library/src/local/library-manager.d.ts +10 -1
  229. package/dist/library/src/local/library-manager.d.ts.map +1 -1
  230. package/dist/library/src/local/library.d.ts +2 -2
  231. package/dist/library/src/local/library.d.ts.map +1 -1
  232. package/dist/library/src/local/local-catalog-source.d.ts +9 -0
  233. package/dist/library/src/local/local-catalog-source.d.ts.map +1 -1
  234. package/dist/library/src/manifest-writer.d.ts +55 -4
  235. package/dist/library/src/manifest-writer.d.ts.map +1 -1
  236. package/dist/library/src/remote/cache.d.ts +18 -1
  237. package/dist/library/src/remote/cache.d.ts.map +1 -1
  238. package/dist/library/src/remote/remote-catalog-source.d.ts.map +1 -1
  239. package/dist/library/src/remote/rest-catalog-source.d.ts.map +1 -1
  240. package/dist/library/src/sync/git-driver.d.ts +7 -6
  241. package/dist/library/src/sync/git-driver.d.ts.map +1 -1
  242. package/dist/{library-publish-VJSWMOLH.js → library-publish-53UTB3MB.js} +151 -42
  243. package/dist/library-publish-53UTB3MB.js.map +1 -0
  244. package/dist/open-library-BOSPJVID.js +40 -0
  245. package/dist/{open-library-DB2PNL2X.js.map → open-library-BOSPJVID.js.map} +1 -1
  246. package/dist/open-registry-4FI7HKET.js +32 -0
  247. package/dist/{open-registry-ZUZJ7MS7.js.map → open-registry-4FI7HKET.js.map} +1 -1
  248. package/dist/paths-SUS4S4YT.js +22 -0
  249. package/dist/{paths-FQ64OLJG.js.map → paths-SUS4S4YT.js.map} +1 -1
  250. package/dist/{plugin-store-MPAMD5OT.js → plugin-store-AGQDMING.js} +9 -9
  251. package/dist/{plugin-store-MPAMD5OT.js.map → plugin-store-AGQDMING.js.map} +1 -1
  252. package/dist/project-I55A4O2T.js +41 -0
  253. package/dist/{project-I6NX3NFB.js.map → project-I55A4O2T.js.map} +1 -1
  254. package/dist/resolver/index.js +8 -8
  255. package/dist/runner/index.js +30 -30
  256. package/dist/runner/src/agent.d.ts.map +1 -1
  257. package/dist/runner/src/serve.d.ts.map +1 -1
  258. package/dist/runner/src/usage-limit-turn-hold.d.ts +51 -0
  259. package/dist/runner/src/usage-limit-turn-hold.d.ts.map +1 -0
  260. package/dist/sdk/asset-manager.js +14 -14
  261. package/dist/sdk/bridge.js +12 -12
  262. package/dist/sdk/core.js +7 -7
  263. package/dist/sdk/flow.js +9 -9
  264. package/dist/sdk/index.js +30 -30
  265. package/dist/sdk/resolver.js +8 -8
  266. package/dist/sdk/runner.js +30 -30
  267. package/dist/{setup-B7JUHYJW.js → setup-7DQJKHUI.js} +22 -22
  268. package/dist/{setup-B7JUHYJW.js.map → setup-7DQJKHUI.js.map} +1 -1
  269. package/dist/{skills-TVO26Y4V.js → skills-NUEN5NDX.js} +13 -13
  270. package/dist/{skills-TVO26Y4V.js.map → skills-NUEN5NDX.js.map} +1 -1
  271. package/dist/source-G2LV5LVD.js +41 -0
  272. package/dist/{source-PJBMDCKX.js.map → source-G2LV5LVD.js.map} +1 -1
  273. package/dist/{source-manifest-publish-LLJWXGID.js → source-manifest-publish-6RM7SWLZ.js} +24 -24
  274. package/dist/{source-manifest-publish-LLJWXGID.js.map → source-manifest-publish-6RM7SWLZ.js.map} +1 -1
  275. package/dist/tui/index.js +61 -41
  276. package/dist/tui/index.js.map +1 -1
  277. package/dist/tui/src/repl.d.ts.map +1 -1
  278. package/dist/types/manifests.js +1 -1
  279. package/dist/types/src/manifests/agent.d.ts +2 -0
  280. package/dist/types/src/manifests/agent.d.ts.map +1 -1
  281. package/dist/types/src/manifests/contract.d.ts +2 -0
  282. package/dist/types/src/manifests/contract.d.ts.map +1 -1
  283. package/dist/types/src/manifests/index.d.ts +3 -3
  284. package/dist/types/src/manifests/index.d.ts.map +1 -1
  285. package/dist/types/src/manifests/preset.d.ts +1 -1
  286. package/dist/types/src/manifests/prompt.d.ts +2 -0
  287. package/dist/types/src/manifests/prompt.d.ts.map +1 -1
  288. package/dist/workspace-plugin/adapters/mcp.js +23 -23
  289. package/dist/workspace-plugin/adapters/omp.js +14 -14
  290. package/dist/workspace-plugin/index.js +2 -2
  291. package/package.json +1 -1
  292. package/dist/chunk-36AFURKZ.js.map +0 -1
  293. package/dist/chunk-4MQS7CAF.js.map +0 -1
  294. package/dist/chunk-7AI3Q5AQ.js.map +0 -1
  295. package/dist/chunk-B6GIW55K.js.map +0 -1
  296. package/dist/chunk-EF7NPDDU.js.map +0 -1
  297. package/dist/chunk-GKMZ6THE.js.map +0 -1
  298. package/dist/chunk-GTILM2CI.js.map +0 -1
  299. package/dist/chunk-IMKSM5NL.js.map +0 -1
  300. package/dist/chunk-JAKOMFC6.js.map +0 -1
  301. package/dist/chunk-JZ5EHBPI.js.map +0 -1
  302. package/dist/chunk-LAMCXQGA.js.map +0 -1
  303. package/dist/chunk-MGGFBZWA.js.map +0 -1
  304. package/dist/chunk-P67WJVP4.js.map +0 -1
  305. package/dist/chunk-QVNRCDTV.js.map +0 -1
  306. package/dist/chunk-UMVKJM4Q.js.map +0 -1
  307. package/dist/chunk-Y7QA2OLD.js.map +0 -1
  308. package/dist/chunk-YSHVQGUS.js.map +0 -1
  309. package/dist/chunk-ZAOGFXDO.js +0 -112
  310. package/dist/chunk-ZAOGFXDO.js.map +0 -1
  311. package/dist/library-publish-VJSWMOLH.js.map +0 -1
  312. package/dist/open-library-DB2PNL2X.js +0 -40
  313. package/dist/open-registry-ZUZJ7MS7.js +0 -32
  314. package/dist/paths-FQ64OLJG.js +0 -22
  315. package/dist/project-I6NX3NFB.js +0 -41
  316. package/dist/source-PJBMDCKX.js +0 -41
@@ -1,20 +1,21 @@
1
- import { getStoreManifestsDir } from './chunk-4MQS7CAF.js';
2
- import { readManifestSourceConfig, isManifestMode, discoverAssetsInTree } from './chunk-Y7QA2OLD.js';
3
- import { installAgent, installTargetOf, canonicalEntryRef, deployAll, removeAsset, createScaffold } from './chunk-YSHVQGUS.js';
1
+ import { unregisteredLibraryNames, renamedLibraryName, scanUnlistedAssets, getStoreManifestsDir, missingLibraryDirMessage } from './chunk-2F5AWQX5.js';
2
+ import { readManifestSourceConfig, isManifestMode, discoverAssetsInTree } from './chunk-J4RBUFGM.js';
3
+ import { installTargetOf, canonicalEntryRef, deployAll, removeAsset, diffDeployedAgent, deployedAssetDrift, createScaffold, installAgent } from './chunk-FH7W6TJ4.js';
4
4
  import { appendHistory, getRecentHistory, clearHistory } from './chunk-KTBKW2FI.js';
5
5
  import { commitChanges } from './chunk-UQ6LFBPZ.js';
6
- import { driverTargetSupportsAgents, CODEX_IDENTITY_DIR } from './chunk-EF7NPDDU.js';
7
- import { resolveConfiguredDriverTarget, GLOBAL_INSTALL_DIR_NAME, resolveLibraryDir, buildLibraryLockEntries, buildLockFile, writeLock, readLock, verifyLock, mergeLockFile, extractForPatch, generatePatch, savePatch, applyPatch } from './chunk-36AFURKZ.js';
8
- import { resolveRuntimeAssets, factoryAssetRoots } from './chunk-GB7WDMH2.js';
6
+ import { driverTargetSupportsAgents, CODEX_IDENTITY_DIR } from './chunk-HZXUXHIV.js';
7
+ import { resolveConfiguredDriverTarget, GLOBAL_INSTALL_DIR_NAME, resolveLibraryDir, buildLibraryLockEntries, buildLockFile, writeLock, readLock, verifyLock, mergeLockFile, extractForPatch, generatePatch, savePatch, applyPatch, computeHash } from './chunk-PBBPZYVG.js';
8
+ import { resolveRuntimeAssets, factoryAssetRoots } from './chunk-EDYHEMJA.js';
9
9
  import { deployedBase, deployedDir, isDeployed, deployedPaths } from './chunk-PMECEZK3.js';
10
- import { resolveSkWorkspaceConfig, effectiveAgentInstances, resolveAgentDefinitionDir, loadMcpServerDeclarations, resolveGlobalHarnesses, resolveGlobalSkWorkspaceConfig, getGlobalCacheDir, sourceCacheKey, getRepoCommit, matchPin, resolveAll, buildProvenanceIndex, ensureRepo, sourceShortName, scanRepo, SKAILE_YAML_DEFAULT, checkRepoStatus, coerceVersion, resolveAsset, readLinks, MAX_PRESET_DEPTH, PresetDepthError, storeManifestSlug } from './chunk-JAKOMFC6.js';
10
+ import { resolveSkWorkspaceConfig, effectiveAgentInstances, loadMcpServerDeclarations, resolveGlobalHarnesses, resolveGlobalSkWorkspaceConfig, getGlobalCacheDir, sourceCacheKey, getRepoCommit, matchPin, buildProvenanceIndex, coerceVersion, sourceShortName, resolveAll, ensureRepo, scanRepo, SKAILE_YAML_DEFAULT, checkRepoStatus, resolveAsset, readLinks, MAX_PRESET_DEPTH, PresetDepthError, DEFAULT_AGENT_INSTANCE_ID, resolveAgentDefinitionDir, storeManifestSlug } from './chunk-ALEYHUC5.js';
11
11
  import { globalSkaileDir, globalCacheRoot, migrateLegacyDir } from './chunk-L5VSIPQD.js';
12
12
  import { DRIVER_TARGETS } from './chunk-CPSIPIX7.js';
13
- import { scanDirectory, parseRequires } from './chunk-GTILM2CI.js';
14
- import { parseAssetRef, parseAssetRefLoose, assetRefToStr, slugifyAssetName, depToStr } from './chunk-QVNRCDTV.js';
13
+ import { loadSkPublishManifest } from './chunk-ZPWTOUTK.js';
14
+ import { scanDirectory, parseRequires, dedupeRequirements } from './chunk-SBSA3IQA.js';
15
+ import { parseAssetRef, parseAssetRefLoose, assetRefToStr, slugifyAssetName, depToStr } from './chunk-BNMZVPYA.js';
15
16
  import { spawnSync } from 'child_process';
16
17
  import { existsSync, rmSync, readdirSync, lstatSync, unlinkSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
17
- import path, { join, resolve, dirname, sep, relative, posix } from 'path';
18
+ import path, { resolve, dirname, sep, join, relative, posix } from 'path';
18
19
  import { gt } from 'semver';
19
20
  import { stringify, parse } from 'yaml';
20
21
  import fs from 'fs/promises';
@@ -36,9 +37,76 @@ function writeLedger(projectDir, ledger) {
36
37
  writeFileSync(p, `${JSON.stringify(ledger, null, 2)}
37
38
  `, "utf-8");
38
39
  }
40
+ function bindInstance(projectDir, id, instance) {
41
+ const errors = [];
42
+ let srcDir;
43
+ try {
44
+ srcDir = resolveAgentDefinitionDir(projectDir, instance.ref);
45
+ } catch (err) {
46
+ errors.push(`agent instance "${id}": ${err instanceof Error ? err.message : String(err)}`);
47
+ }
48
+ if (srcDir !== void 0 && !existsSync(join(srcDir, "agent.yaml"))) {
49
+ errors.push(
50
+ `agent instance "${id}": ${instance.ref} resolves to ${srcDir}, which has no agent.yaml`
51
+ );
52
+ }
53
+ return { srcDir, errors };
54
+ }
55
+ function unboundAgentInstances(projectDir, driverTarget) {
56
+ if (!driverTargetSupportsAgents(driverTarget)) return [];
57
+ let instances;
58
+ try {
59
+ instances = effectiveAgentInstances(resolveSkWorkspaceConfig(projectDir));
60
+ } catch {
61
+ return [];
62
+ }
63
+ const out = [];
64
+ for (const [id, instance] of Object.entries(instances)) {
65
+ const { errors } = bindInstance(projectDir, id, instance);
66
+ if (errors.length > 0) out.push({ id, ref: instance.ref, error: errors.join("; ") });
67
+ }
68
+ return out;
69
+ }
70
+ function removeAgentInstanceRenderings(opts) {
71
+ const { projectDir, driverTarget, ids } = opts;
72
+ if (!driverTargetSupportsAgents(driverTarget) || ids.length === 0) return [];
73
+ const ledger = readLedger(projectDir);
74
+ const previous = ledger.targets[driverTarget];
75
+ if (!previous) return [];
76
+ const gone = new Set(ids);
77
+ const claimed = new Set(
78
+ Object.entries(previous).filter(([id]) => !gone.has(id)).flatMap(([, files]) => files)
79
+ );
80
+ const agentDeployDir = deployedBase("agent", driverTarget, false, projectDir);
81
+ const removable = [resolve(agentDeployDir), resolve(dirname(agentDeployDir), CODEX_IDENTITY_DIR)];
82
+ const root = resolve(projectDir);
83
+ const removed = [];
84
+ for (const id of gone) {
85
+ for (const rel of previous[id] ?? []) {
86
+ if (claimed.has(rel)) continue;
87
+ const abs = resolve(root, rel);
88
+ if (!removable.some((dir) => abs.startsWith(dir + sep)) || !existsSync(abs)) continue;
89
+ rmSync(abs, { recursive: true, force: true });
90
+ removed.push(rel);
91
+ }
92
+ }
93
+ const owned = Object.fromEntries(Object.entries(previous).filter(([id]) => !gone.has(id)));
94
+ const nextTargets = { ...ledger.targets };
95
+ if (Object.keys(owned).length > 0) nextTargets[driverTarget] = owned;
96
+ else delete nextTargets[driverTarget];
97
+ if (JSON.stringify(nextTargets) !== JSON.stringify(ledger.targets)) {
98
+ writeLedger(projectDir, { version: 1, targets: nextTargets });
99
+ }
100
+ return removed;
101
+ }
39
102
  function installAgentInstances(opts) {
40
103
  const { projectDir, driverTarget, log } = opts;
41
- const result = { instances: [], removed: [], warnings: [] };
104
+ const result = {
105
+ instances: [],
106
+ removed: [],
107
+ warnings: [],
108
+ errors: []
109
+ };
42
110
  if (!driverTargetSupportsAgents(driverTarget)) return result;
43
111
  const wsConfig = opts.wsConfig ?? resolveSkWorkspaceConfig(projectDir);
44
112
  const instances = effectiveAgentInstances(wsConfig);
@@ -48,20 +116,9 @@ function installAgentInstances(opts) {
48
116
  const previous = ledger.targets[driverTarget] ?? {};
49
117
  const owned = {};
50
118
  for (const [id, instance] of Object.entries(instances)) {
51
- let srcDir;
52
- try {
53
- srcDir = resolveAgentDefinitionDir(projectDir, instance.ref);
54
- } catch (err) {
55
- result.warnings.push(
56
- `agent instance "${id}": ${err instanceof Error ? err.message : String(err)}`
57
- );
58
- }
59
- if (srcDir === void 0 || !existsSync(join(srcDir, "agent.yaml"))) {
60
- if (srcDir !== void 0) {
61
- result.warnings.push(
62
- `agent instance "${id}": ${instance.ref} resolves to ${srcDir}, which has no agent.yaml`
63
- );
64
- }
119
+ const { srcDir, errors } = bindInstance(projectDir, id, instance);
120
+ result.errors.push(...errors);
121
+ if (srcDir === void 0 || errors.length > 0) {
65
122
  if (previous[id]) owned[id] = previous[id];
66
123
  continue;
67
124
  }
@@ -273,7 +330,30 @@ async function renderMcpServerConfig(projectDir, driverTarget, opts) {
273
330
 
274
331
  // asset-manager/src/index.ts
275
332
  function normalizeRepoUrl(url) {
276
- return url.trim().replace(/^git@([^:]+):/, "https://$1/").replace(/^[a-z]+:\/\//i, "").replace(/\.git$/i, "").replace(/\/+$/, "").toLowerCase();
333
+ return url.trim().replace(/^git@([^:]+):/, "https://$1/").replace(/^[a-z]+:\/\//i, "").replace(/\/+$/, "").replace(/\.git$/i, "").replace(/\/+$/, "").toLowerCase();
334
+ }
335
+ function withoutUrlSecrets(url) {
336
+ if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(url)) return url;
337
+ try {
338
+ const parsed = new URL(url);
339
+ if (parsed.protocol === "http:" || parsed.protocol === "https:" || parsed.password) {
340
+ parsed.username = "";
341
+ }
342
+ parsed.password = "";
343
+ parsed.search = "";
344
+ parsed.hash = "";
345
+ return parsed.toString();
346
+ } catch {
347
+ return url.replace(/[?#].*$/, "").replace(/^([a-z][a-z0-9+.-]*:\/\/).*@/i, "$1");
348
+ }
349
+ }
350
+ function libraryRemoteUrl(library) {
351
+ const config = library.backendConfig;
352
+ if (library.backend !== "git" || !("url" in config) || typeof config.url !== "string") {
353
+ return void 0;
354
+ }
355
+ const url = config.url.trim();
356
+ return url ? withoutUrlSecrets(url) : void 0;
277
357
  }
278
358
  function storeOverlayPathForUrl(url) {
279
359
  return join(getStoreManifestsDir(), `${storeManifestSlug(url)}.yaml`);
@@ -375,6 +455,26 @@ function libraryDiskHashes(libPath, cache, registry) {
375
455
  }
376
456
  return hashes;
377
457
  }
458
+ function manifestFileHasAssetsArray(libPath) {
459
+ try {
460
+ const doc = parse(readFileSync(join(libPath, "skaile.manifest.yaml"), "utf8"));
461
+ return Array.isArray(doc?.assets);
462
+ } catch {
463
+ return false;
464
+ }
465
+ }
466
+ function isUnlistedLibraryAsset(entry, libPath) {
467
+ const manifest = loadSkPublishManifest(libPath);
468
+ const listed = manifest?.assets ?? [];
469
+ if (!manifest || manifest.source) return false;
470
+ if (listed.length === 0 && !manifestFileHasAssetsArray(libPath)) return false;
471
+ const file = resolve(entry.source);
472
+ return !listed.some((a) => {
473
+ if (!a.root || a.source) return false;
474
+ const root = resolve(libPath, a.root);
475
+ return file === root || file.startsWith(root + sep);
476
+ });
477
+ }
378
478
  function withLibraryFiles(entry, libPath, cache, registry) {
379
479
  let byManifest = cache.get(libPath);
380
480
  if (!byManifest) {
@@ -539,6 +639,10 @@ function keptLockEntries(prev, missing) {
539
639
  }
540
640
  return out;
541
641
  }
642
+ function presetsLockSlice(presets) {
643
+ const refs = [...new Set(presets.map((p) => p.ref))].sort();
644
+ return refs.length > 0 ? { presets: refs } : {};
645
+ }
542
646
  function withKeptLockEntries(lock, kept) {
543
647
  if (Object.keys(kept.assets).length === 0) return lock;
544
648
  const sources = new Map(lock.sources.map((s) => [s.url, s]));
@@ -567,8 +671,45 @@ function relockedRefs(prev, next) {
567
671
  }
568
672
  return out;
569
673
  }
570
- function unregisteredLibraryFix(library, ref) {
571
- return `register library "${library}" again with \`skaile library init ${library} --path <dir>\`, or drop the dependency with \`skaile remove ${ref}\``;
674
+ function libraryMoves(prev, next) {
675
+ if (!prev) return [];
676
+ const out = [];
677
+ for (const [key, entry] of Object.entries(next)) {
678
+ const to = entry.library;
679
+ if (!to || to.installed) continue;
680
+ const id = parseCanonicalLockKey(key);
681
+ for (const [prevKey, before] of Object.entries(prev.assets)) {
682
+ const o = parseCanonicalLockKey(prevKey);
683
+ if (o.kind !== id.kind || o.name !== id.name || o.publisher !== id.publisher) continue;
684
+ const from = before.library;
685
+ if (!from || from.installed || from.name === to.name) continue;
686
+ out.push({ ref: `${id.kind}:@${id.publisher}/${id.name}`, from: from.name, to: to.name });
687
+ break;
688
+ }
689
+ }
690
+ return out;
691
+ }
692
+ function unregisteredLibraryFix(origin, ref) {
693
+ const register = origin.url ? `\`skaile library init ${origin.name} --git ${origin.url}\` (no registered library has the remote ${origin.url} either)` : `\`skaile library init ${origin.name} --path <dir>\``;
694
+ return `register library "${origin.name}" again with ${register}, or drop the dependency with \`skaile remove ${ref}\``;
695
+ }
696
+ function declaredNoHolderReason(ref) {
697
+ return `is declared but no library or source holds it \u2014 register the library that holds it with \`skaile library init <name> --path <dir>\`, or drop it with \`skaile remove ${ref.split("#")[0]}\``;
698
+ }
699
+ var REGISTER_HOLDER = "register the library that holds it with `skaile library init <name> --path <dir>`";
700
+ function requirementNoHolderFix(asset) {
701
+ return `${REGISTER_HOLDER}, or drop ${asset} with \`skaile remove ${asset}\``;
702
+ }
703
+ function sameAsset(ref, want) {
704
+ const p = parseRefTolerant(ref);
705
+ return p !== null && p.kind === want.kind && p.name === want.name && (!want.publisher || !p.publisher || p.publisher === want.publisher);
706
+ }
707
+ function lockedLibraryProblem(lookup, origin, ref) {
708
+ return lookup.state === "dir-missing" ? missingLibraryDirMessage({ name: origin.name, path: lookup.path }) : `library "${origin.name}" is not registered \u2014 renamed or removed? ${unregisteredLibraryFix(origin, ref)}`;
709
+ }
710
+ function gapIdentity(ref) {
711
+ const p = parseRefTolerant(ref);
712
+ return p ? `${p.kind}:${p.name}` : ref;
572
713
  }
573
714
  function memberIdentity(ref) {
574
715
  const parsed = parseRefTolerant(ref.trim());
@@ -726,13 +867,15 @@ var AssetManager = class {
726
867
  }
727
868
  /** Deploy options for one harness. In global mode, `driverTarget` is the
728
869
  * fan-out target (not the constructor's `driverTarget`, which is project-only). */
729
- deployOptsFor(target, link) {
870
+ deployOptsFor(target, link, onSkippedSymlink, onEnvFile) {
730
871
  return {
731
872
  registry: this.registry,
732
873
  driverTarget: target,
733
874
  global: this.global,
734
875
  cwd: this.projectDir,
735
- link: link === true
876
+ link: link === true,
877
+ ...onSkippedSymlink ? { onSkippedSymlink: (entry, p) => onSkippedSymlink(canonicalEntryRef(entry), p) } : {},
878
+ ...onEnvFile ? { onEnvFile: (entry, p) => onEnvFile(canonicalEntryRef(entry), p) } : {}
736
879
  };
737
880
  }
738
881
  /**
@@ -759,18 +902,27 @@ var AssetManager = class {
759
902
  * Render every agent instance of `skaile.yaml` into the driver target's native
760
903
  * agent files (`.claude/agents/<id>.md`, …) and prune the renderings of
761
904
  * instances that left the config. Runs after the skills deploy, so composed
762
- * skills resolve. Project scope only. Never fails an install — a render error
763
- * is warned and swallowed.
905
+ * skills resolve. Project scope only. An instance whose `ref` resolves to no agent is
906
+ * reported in `agentInstanceErrors`; any other render error is warned and swallowed.
764
907
  */
765
- renderAgentInstances() {
908
+ async renderAgentInstances() {
766
909
  if (this.global) return {};
767
910
  try {
768
911
  const result = installAgentInstances({
769
912
  projectDir: this.projectDir,
770
913
  driverTarget: this.driverTarget
771
914
  });
772
- for (const w of result.warnings) console.warn(`skaile: ${w}`);
773
- return { agentInstances: result.instances, agentFilesRemoved: result.removed };
915
+ for (const u of unboundAgentInstances(this.projectDir, this.driverTarget)) {
916
+ if (!u.ref.startsWith("agent:")) continue;
917
+ const at = result.errors.findIndex((e) => e.startsWith(`agent instance "${u.id}":`));
918
+ if (at >= 0) result.errors[at] += ` \u2014 ${(await this.unboundAgentAdvice(u.ref)).advice}`;
919
+ }
920
+ for (const w of [...result.warnings, ...result.errors]) console.warn(`skaile: ${w}`);
921
+ return {
922
+ agentInstances: result.instances,
923
+ agentFilesRemoved: result.removed,
924
+ ...result.errors.length > 0 ? { agentInstanceErrors: result.errors } : {}
925
+ };
774
926
  } catch (err) {
775
927
  console.warn(
776
928
  `skaile: could not render agent instances for ${this.driverTarget}: ${err instanceof Error ? err.message : String(err)}`
@@ -902,6 +1054,11 @@ var AssetManager = class {
902
1054
  *
903
1055
  * @param opts.quiet - Skip the cross-library collision warning (a lookup that only
904
1056
  * builds an error hint must not warn).
1057
+ * @param opts.library - Resolve only from this library (the one a lock entry names), not
1058
+ * from every registered one. Never warns: no choice is made between libraries.
1059
+ * @param opts.lockedLibraries - `kind:publisher/name` → the library its lock entry names
1060
+ * ({@link AssetManager.lockedLibraryNames}). A ref listed here resolves from that library
1061
+ * when it holds a match, whatever the default library is.
905
1062
  * @returns the matched deploy entries, the set of `missing` refs covered, and
906
1063
  * each entry's originating library name.
907
1064
  */
@@ -914,6 +1071,7 @@ var AssetManager = class {
914
1071
  const { byKey, isDefaultLib, libraryDir } = scan;
915
1072
  const entries = [];
916
1073
  const fileSets = /* @__PURE__ */ new Map();
1074
+ let snap;
917
1075
  const allCandidates = [...byKey.values()].flat();
918
1076
  for (const ref of missing) {
919
1077
  const parsed = parseRefTolerant(ref);
@@ -921,21 +1079,54 @@ var AssetManager = class {
921
1079
  const hash = ref.indexOf("#");
922
1080
  const pin = parsed.pin ?? (hash >= 0 ? ref.slice(hash + 1) : void 0);
923
1081
  const key = parsed.publisher ? `${parsed.kind}:@${parsed.publisher}/${parsed.name}` : void 0;
924
- const pool = key ? byKey.get(key) ?? [] : allCandidates.filter((c) => c.entry.kind === parsed.kind && c.entry.name === parsed.name);
925
- const candidates = pool.filter((c) => !pin || matchPin(pin, c.entry.version ?? ""));
1082
+ const scanned = (key ? byKey.get(key) ?? [] : allCandidates.filter(
1083
+ (c) => c.entry.kind === parsed.kind && c.entry.name === parsed.name
1084
+ )).filter((c) => !opts.library || c.library === opts.library);
1085
+ const pool = [];
1086
+ for (const c of scanned) {
1087
+ if (c.entry.version) {
1088
+ pool.push(c);
1089
+ continue;
1090
+ }
1091
+ if (snap === void 0) snap = await this.loadLibraryIndex();
1092
+ const def = newestLibraryDef(snap, c.library, {
1093
+ kind: c.entry.kind,
1094
+ publisher: c.entry.publisher ?? "",
1095
+ name: c.entry.name
1096
+ });
1097
+ pool.push(def?.version ? { ...c, entry: { ...c.entry, version: def.version } } : c);
1098
+ }
1099
+ let candidates = pool.filter((c) => !pin || matchPin(pin, c.entry.version ?? ""));
1100
+ let lockedLibrary;
1101
+ if (parsed.publisher) {
1102
+ lockedLibrary = opts.lockedLibraries?.get(
1103
+ `${parsed.kind}:${parsed.publisher.replace(/^@/, "")}/${parsed.name}`
1104
+ );
1105
+ } else {
1106
+ const same = [...opts.lockedLibraries ?? []].filter(([k]) => {
1107
+ const slash = k.indexOf("/");
1108
+ return k.startsWith(`${parsed.kind}:`) && k.slice(slash + 1) === parsed.name;
1109
+ });
1110
+ if (same.length === 1) lockedLibrary = same[0][1];
1111
+ }
1112
+ if (lockedLibrary) {
1113
+ const inLocked = candidates.filter((c) => c.library === lockedLibrary);
1114
+ if (inLocked.length > 0) candidates = inLocked;
1115
+ }
926
1116
  if (candidates.length === 0) continue;
927
1117
  candidates.sort(libraryPreference(isDefaultLib));
928
1118
  const hit = candidates[0];
929
- if (!opts.quiet) {
1119
+ if (!opts.quiet && !opts.library) {
930
1120
  this.warnLibraryCollision(
931
1121
  `${hit.entry.kind}:@${hit.entry.publisher}/${hit.entry.name}`,
932
1122
  candidates,
933
1123
  hit
934
1124
  );
935
1125
  }
936
- withLibraryFiles(hit.entry, libraryDir.get(hit.library), fileSets, this.registry);
937
- entries.push(hit.entry);
938
- libraryOf.set(hit.entry, hit.library);
1126
+ const entry = hit.entry;
1127
+ withLibraryFiles(entry, libraryDir.get(hit.library), fileSets, this.registry);
1128
+ entries.push(entry);
1129
+ libraryOf.set(entry, hit.library);
939
1130
  resolved.add(ref);
940
1131
  }
941
1132
  return { entries, resolved, libraryOf };
@@ -955,11 +1146,12 @@ var AssetManager = class {
955
1146
  if (existsSync(l.path)) roots.push({ name: l.name, dir: l.path, isDefault: l.isDefault });
956
1147
  }
957
1148
  const registered = new Set(roots.map((r) => r.name));
1149
+ const unregistered = unregisteredLibraryNames();
958
1150
  try {
959
1151
  const librariesRoot = resolveLibraryDir();
960
1152
  if (existsSync(librariesRoot)) {
961
1153
  for (const dirent of readdirSync(librariesRoot, { withFileTypes: true })) {
962
- if (dirent.isDirectory() && dirent.name !== GLOBAL_INSTALL_DIR_NAME && !registered.has(dirent.name)) {
1154
+ if (dirent.isDirectory() && dirent.name !== GLOBAL_INSTALL_DIR_NAME && !registered.has(dirent.name) && !unregistered.has(dirent.name)) {
963
1155
  roots.push({
964
1156
  name: dirent.name,
965
1157
  dir: join(librariesRoot, dirent.name),
@@ -1012,10 +1204,117 @@ var AssetManager = class {
1012
1204
  }
1013
1205
  return out;
1014
1206
  }
1207
+ /**
1208
+ * Every asset a registered library holds, read from the library directories themselves
1209
+ * (not the local index, which keeps one row per ref and version and loses a ref when
1210
+ * another library re-publishes it). One row per holder, all versions.
1211
+ *
1212
+ * @returns `[]` when no library holds an asset. Never warns.
1213
+ */
1214
+ async libraryAssets() {
1215
+ const scan = await this.scanLibraries();
1216
+ if (!scan) return [];
1217
+ return [...scan.byKey.values()].flat();
1218
+ }
1219
+ /**
1220
+ * Every origin that holds `ref` (`kind:@publisher/name`) with the version it holds, in
1221
+ * the order `add <ref>` prefers them, so the first row is the one `add` deploys: the
1222
+ * project's source clones and the bundled factory source (highest version first), then
1223
+ * the registered libraries. `add`, `info` and `search` all read this, so the three name
1224
+ * one origin for one ref. Stores are not asked (no network).
1225
+ *
1226
+ * @returns one row per origin and version; `[]` for a ref without a publisher or one
1227
+ * nothing holds. Never warns.
1228
+ */
1229
+ async refOrigins(ref) {
1230
+ const parsed = parseRefTolerant(ref);
1231
+ if (!parsed?.publisher) return [];
1232
+ const key = `${parsed.kind}:@${parsed.publisher}/${parsed.name}`;
1233
+ return (await this.collectOrigins(key)).get(key) ?? [];
1234
+ }
1235
+ /**
1236
+ * Every ref (`kind:@publisher/name`) that several origins hold at different versions,
1237
+ * with its origins in {@link refOrigins} order. A ref the origins hold at one version
1238
+ * has nothing to choose between and is left out.
1239
+ */
1240
+ async competingOrigins() {
1241
+ const out = /* @__PURE__ */ new Map();
1242
+ for (const [key, origins] of await this.collectOrigins()) {
1243
+ if (new Set(origins.map((o) => o.version)).size > 1) out.set(key, origins);
1244
+ }
1245
+ return out;
1246
+ }
1247
+ /**
1248
+ * The origins of {@link refOrigins} for every ref, or for the one `only` names
1249
+ * (`kind:@publisher/name`), keyed by `kind:@publisher/name`.
1250
+ */
1251
+ async collectOrigins(only) {
1252
+ const { repositories, sources } = this.loadConfig();
1253
+ this.ensureSourceClonesQuiet(repositories, sources);
1254
+ const clones = this.buildClones(repositories);
1255
+ const cloneDirs = new Set(clones.map((c) => resolve(c.localPath)));
1256
+ const index = await buildProvenanceIndex(clones, {
1257
+ projectDir: this.projectDir,
1258
+ knownKinds: this.knownKinds
1259
+ });
1260
+ const factoryUrl = normalizeRepoUrl(FACTORY_SOURCE_URL);
1261
+ const hasFactory = Object.values(repositories).some((d) => d.factory);
1262
+ const out = /* @__PURE__ */ new Map();
1263
+ const originsOf = (key) => {
1264
+ let list = out.get(key);
1265
+ if (!list) {
1266
+ list = [];
1267
+ out.set(key, list);
1268
+ }
1269
+ return list;
1270
+ };
1271
+ for (const candidates of index.values()) {
1272
+ const first = candidates[0];
1273
+ if (!first) continue;
1274
+ const key = `${first.kind}:@${first.publisher}/${first.name}`;
1275
+ if (only !== void 0 && key !== only) continue;
1276
+ const held = [...candidates].sort((a, b) => {
1277
+ const va = coerceVersion(a.version);
1278
+ const vb = coerceVersion(b.version);
1279
+ if (gt(va, vb)) return -1;
1280
+ if (gt(vb, va)) return 1;
1281
+ return a.sourceUrl.localeCompare(b.sourceUrl);
1282
+ });
1283
+ for (const c of held) {
1284
+ const factory = hasFactory && normalizeRepoUrl(c.sourceUrl) === factoryUrl;
1285
+ originsOf(key).push({
1286
+ kind: factory ? "factory" : "source",
1287
+ label: factory ? "factory" : sourceShortName(c.sourceUrl),
1288
+ version: c.version,
1289
+ description: c.description ?? ""
1290
+ });
1291
+ }
1292
+ }
1293
+ const scan = await this.scanLibraries();
1294
+ const prefer = libraryPreference(scan?.isDefaultLib ?? /* @__PURE__ */ new Map());
1295
+ for (const [key, holders] of scan?.byKey ?? []) {
1296
+ if (only !== void 0 && key !== only) continue;
1297
+ for (const c of [...holders].sort(prefer)) {
1298
+ const dir = scan?.libraryDir.get(c.library);
1299
+ if (dir && cloneDirs.has(resolve(dir))) continue;
1300
+ originsOf(key).push({
1301
+ kind: "library",
1302
+ label: c.library,
1303
+ version: c.entry.version ?? "",
1304
+ description: c.entry.description ?? ""
1305
+ });
1306
+ }
1307
+ }
1308
+ return out;
1309
+ }
1015
1310
  /**
1016
1311
  * Where each missed publisher-qualified ref went when its publisher changed, for every
1017
1312
  * ref at once (one library scan, one lock read):
1018
- * - `moved` when a registered library holds the same kind + name under another publisher;
1313
+ * - `moved` when a registered library holds the same kind + name under another publisher.
1314
+ * When the lock ties the missed ref to a library, only that library is looked at (an
1315
+ * asset of the same name in another library is a different asset). Without a lock
1316
+ * entry, a library asset the project already declares or locks under its own ref is
1317
+ * not offered, and two candidate publishers are ambiguous: no hint;
1019
1318
  * - `lockedAs` when the lock holds the same kind + name under another publisher, records
1020
1319
  * the library it came from, and that library now holds the asset under yet another
1021
1320
  * publisher than the locked one. An unrelated asset that merely shares kind + name does
@@ -1035,11 +1334,28 @@ var AssetManager = class {
1035
1334
  }
1036
1335
  if (wanted.size === 0) return out;
1037
1336
  const lockEntries = Object.entries(this.tryReadLock()?.assets ?? {});
1038
- const bare = [...new Set([...wanted.values()].map((w) => `${w.kind}:${w.name}`))];
1039
- const lib = await this.resolveLibraryEntries(bare, { quiet: true });
1337
+ const scan = await this.scanLibraries();
1338
+ if (!scan) return out;
1339
+ const held = /* @__PURE__ */ new Set();
1340
+ for (const [key] of lockEntries) {
1341
+ const { publisher, kind, name } = parseCanonicalLockKey(key);
1342
+ if (publisher && kind && name) held.add(`${publisher}/${kind}:${name}`);
1343
+ }
1344
+ for (const d of this.loadConfig().dependencies) {
1345
+ const id = refIdentity(d);
1346
+ if (id !== null) held.add(id);
1347
+ }
1040
1348
  for (const [ref, w] of wanted) {
1041
- const moved = lib.entries.find((e) => e.kind === w.kind && e.name === w.name);
1042
- const movedLibrary = moved ? lib.libraryOf.get(moved) : void 0;
1349
+ const origin = lockEntries.map(([key, e]) => ({ id: parseCanonicalLockKey(key), library: e.library })).find(
1350
+ ({ id, library }) => library && !library.installed && id.kind === w.kind && id.name === w.name && id.publisher === w.publisher
1351
+ )?.library?.name;
1352
+ const pool = [...scan.byKey.values()].flat().filter(
1353
+ (c) => c.entry.kind === w.kind && c.entry.name === w.name && c.entry.publisher !== w.publisher && (origin ? c.library === origin : !held.has(`${c.entry.publisher}/${c.entry.kind}:${c.entry.name}`))
1354
+ );
1355
+ const publishers = new Set(pool.map((c) => c.entry.publisher));
1356
+ const best = publishers.size === 1 ? pool.sort(libraryPreference(scan.isDefaultLib))[0] : void 0;
1357
+ const moved = best?.entry;
1358
+ const movedLibrary = best?.library;
1043
1359
  const finding = {};
1044
1360
  for (const [key, entry] of lockEntries) {
1045
1361
  const id = parseCanonicalLockKey(key);
@@ -1104,17 +1420,20 @@ var AssetManager = class {
1104
1420
  if (!origin) continue;
1105
1421
  const plain = want.publisher ? `${want.kind}:@${want.publisher}/${want.name}` : `${want.kind}:${want.name}`;
1106
1422
  const moved = findings.get(ref)?.moved;
1423
+ const lookup = this.lockedLibrary(origin, snap);
1107
1424
  if (moved?.library && want.publisher) {
1108
1425
  out.set(
1109
1426
  ref,
1110
1427
  `did you mean ${moved.ref}? Change the ref in skaile.yaml to it, or keep the old ref with \`skaile library set ${moved.library} publisher=${want.publisher}\``
1111
1428
  );
1112
- } else if (!this.libraryPath(origin.name, snap)) {
1113
- out.set(ref, unregisteredLibraryFix(origin.name, plain));
1429
+ } else if (lookup.state === "dir-missing") {
1430
+ out.set(ref, lockedLibraryProblem(lookup, origin, plain));
1431
+ } else if (lookup.state === "unregistered") {
1432
+ out.set(ref, unregisteredLibraryFix(origin, plain));
1114
1433
  } else {
1115
1434
  out.set(
1116
1435
  ref,
1117
- `library "${origin.name}" no longer holds it \u2014 restore it there, or drop the dependency with \`skaile remove ${plain}\``
1436
+ `library "${lookup.name}" no longer holds it \u2014 restore it there, or drop the dependency with \`skaile remove ${plain}\``
1118
1437
  );
1119
1438
  }
1120
1439
  }
@@ -1214,27 +1533,120 @@ var AssetManager = class {
1214
1533
  * preferring it would lock a stale version after an in-place bump. The hash is
1215
1534
  * computed from the files on disk, so it matches what `install --locked`
1216
1535
  * re-hashes.
1536
+ *
1537
+ * A git-backed library also records its remote URL. When the previous lock names the
1538
+ * library under another name that {@link lockedLibrary} matched to this one by URL (a
1539
+ * teammate registered the same remote under a different name), the entry keeps the locked
1540
+ * name and URL: the lock does not change because it was installed on another machine.
1217
1541
  */
1218
1542
  async libraryLockEntries(lib) {
1219
1543
  if (lib.entries.length === 0) return {};
1220
1544
  const snap = await this.loadLibraryIndex();
1545
+ const previous = Object.entries(this.tryReadLock()?.assets ?? {}).filter(
1546
+ ([, e]) => e.library !== void 0 && !e.library.installed
1547
+ );
1221
1548
  const diskHashes = /* @__PURE__ */ new Map();
1222
1549
  return buildLibraryLockEntries(
1223
1550
  lib.entries.map((e) => {
1224
1551
  const name = lib.libraryOf.get(e) ?? "";
1225
1552
  const publisher = e.publisher ?? "";
1226
1553
  const def = newestLibraryDef(snap, name, { kind: e.kind, publisher, name: e.name });
1227
- const libPath = this.libraryPath(name, snap);
1554
+ const libPath = this.libraryPath({ name }, snap);
1228
1555
  const sha256 = (libPath ? libraryDiskHashes(libPath, diskHashes, this.registry).get(`${e.kind}:${e.name}`) : void 0) ?? def?.sha256;
1556
+ const registered = snap?.libraries.find((l) => l.name === name);
1557
+ let lockName = name;
1558
+ let url = registered ? libraryRemoteUrl(registered) : void 0;
1559
+ const locked = previous.find(([key]) => {
1560
+ const id = parseCanonicalLockKey(key);
1561
+ return id.kind === e.kind && id.name === e.name && id.publisher === publisher;
1562
+ })?.[1].library;
1563
+ if (locked) {
1564
+ const lookup = this.lockedLibrary(locked, snap);
1565
+ if (lookup.state === "ok" && lookup.name === name) {
1566
+ lockName = locked.name;
1567
+ if (locked.url && url && normalizeRepoUrl(locked.url) === normalizeRepoUrl(url)) {
1568
+ url = locked.url;
1569
+ }
1570
+ }
1571
+ }
1229
1572
  return {
1230
1573
  kind: e.kind,
1231
1574
  publisher,
1232
1575
  name: e.name,
1233
- library: { name, version: e.version || def?.version || "0.0.0", sha256 }
1576
+ library: {
1577
+ name: lockName,
1578
+ version: e.version || def?.version || "0.0.0",
1579
+ sha256,
1580
+ ...url ? { url } : {}
1581
+ }
1234
1582
  };
1235
1583
  })
1236
1584
  );
1237
1585
  }
1586
+ /**
1587
+ * Lock keys that `skaile.yaml` still wants: the entries a declared ref names, plus every
1588
+ * entry such an asset requires, transitively. All keys when a `preset:` is declared.
1589
+ */
1590
+ async declaredLockKeys(lock, declared) {
1591
+ const keys = Object.keys(lock.assets);
1592
+ const wanted = declared.map((d) => parseRefTolerant(d.split(" ")[0] ?? d));
1593
+ if (wanted.some((w) => w?.kind === "preset")) return new Set(keys);
1594
+ const matching = (want) => keys.filter((key) => {
1595
+ const id = parseCanonicalLockKey(key);
1596
+ return id.kind === want.kind && id.name === want.name && (!want.publisher || !id.publisher || id.publisher === want.publisher);
1597
+ });
1598
+ const inScope = /* @__PURE__ */ new Set();
1599
+ const queue = [];
1600
+ const add = (key) => {
1601
+ if (inScope.has(key)) return;
1602
+ inScope.add(key);
1603
+ queue.push(key);
1604
+ };
1605
+ for (const w of wanted) if (w) matching(w).forEach(add);
1606
+ for (const key of queue) {
1607
+ for (const dep of await this.deployedRequirementsOf(parseCanonicalLockKey(key))) {
1608
+ const want = memberIdentity(dep);
1609
+ if (want) matching(want).forEach(add);
1610
+ }
1611
+ }
1612
+ return inScope;
1613
+ }
1614
+ /**
1615
+ * The `Missing:` lines of every library-origin lock entry whose library cannot supply
1616
+ * assets on this machine (not registered, or registered with its directory gone). Plain
1617
+ * `install` stops on a non-empty list, the same lines `install --locked` prints: it must
1618
+ * not re-resolve a locked asset from another origin. The ONE function that decides "the
1619
+ * locked library is not here", so a lock-recorded URL fallback extends it in one place.
1620
+ *
1621
+ * Only entries `skaile.yaml` still declares, or that a declared asset requires
1622
+ * (transitively, read from the deployed copy), count. A stale entry nothing declares is
1623
+ * pruned by `install` as usual, so a removed dependency whose library is gone can be
1624
+ * cleaned up. With a declared `preset:` every entry counts: the lock records no edge
1625
+ * from a preset to its items, and the preset's own library may be the one that is gone.
1626
+ *
1627
+ * @param declared - The `dependencies:` refs as written in `skaile.yaml`.
1628
+ */
1629
+ async unavailableLockedLibraries(lock, declared) {
1630
+ const inScope = await this.declaredLockKeys(lock, declared);
1631
+ const locked = Object.entries(lock.assets).filter(
1632
+ ([key, e]) => e.library !== void 0 && !e.library.installed && inScope.has(key)
1633
+ );
1634
+ if (locked.length === 0) return [];
1635
+ const snap = await this.loadLibraryIndex();
1636
+ const lines = [];
1637
+ for (const [ref, e] of locked) {
1638
+ const { publisher, kind, name } = parseCanonicalLockKey(ref);
1639
+ if (!publisher || !kind || !name) continue;
1640
+ const lookup = this.lockedLibrary(e.library, snap);
1641
+ if (lookup.state === "ok") continue;
1642
+ const component = this.installTarget(kind);
1643
+ const deployed = component !== void 0 && isDeployed(component, name, this.driverTarget, this.global, this.projectDir);
1644
+ const problem = lockedLibraryProblem(lookup, e.library, `${kind}:@${publisher}/${name}`);
1645
+ const kept = deployed ? "the deployed copy and its lock entry are kept" : "its lock entry is kept; no deployed copy exists";
1646
+ lines.push(`${ref} (${problem}; ${kept})`);
1647
+ }
1648
+ return lines;
1649
+ }
1238
1650
  /**
1239
1651
  * Re-resolve every library-origin lock entry from its named library, for
1240
1652
  * `install --locked`. An entry is restored only when its library is still
@@ -1258,25 +1670,26 @@ var AssetManager = class {
1258
1670
  missing.push(`${ref} (unparseable lock key)`);
1259
1671
  continue;
1260
1672
  }
1261
- const libPath = this.libraryPath(origin.name, snap);
1262
- if (!libPath) {
1673
+ const lookup = this.lockedLibrary(origin, snap);
1674
+ if (lookup.state !== "ok") {
1263
1675
  missing.push(
1264
- `${ref} (library "${origin.name}" is not registered \u2014 renamed or removed? ${unregisteredLibraryFix(origin.name, `${kind}:@${publisher}/${name}`)}; \`skaile install\` re-locks it when another library holds it now)`
1676
+ `${ref} (${lockedLibraryProblem(lookup, origin, `${kind}:@${publisher}/${name}`)})`
1265
1677
  );
1266
1678
  continue;
1267
1679
  }
1680
+ const libPath = lookup.path;
1268
1681
  const found = (await scanDirectory(libPath, publisher, { knownKinds: this.knownKinds })).find(
1269
1682
  (x) => x.kind === kind && x.name === name
1270
1683
  );
1271
1684
  if (!found) {
1272
- missing.push(`${ref} (not found in library "${origin.name}")`);
1685
+ missing.push(`${ref} (not found in library "${lookup.name}")`);
1273
1686
  continue;
1274
1687
  }
1275
- const def = newestLibraryDef(snap, origin.name, { kind, publisher, name });
1688
+ const def = newestLibraryDef(snap, lookup.name, { kind, publisher, name });
1276
1689
  const current = found.version || def?.version || "0.0.0";
1277
1690
  if (current !== origin.version) {
1278
1691
  missing.push(
1279
- `${ref} (library "${origin.name}" is at ${current}, locked ${origin.version})`
1692
+ `${ref} (library "${lookup.name}" is at ${current}, locked ${origin.version})`
1280
1693
  );
1281
1694
  continue;
1282
1695
  }
@@ -1284,29 +1697,108 @@ var AssetManager = class {
1284
1697
  const onDisk = libraryDiskHashes(libPath, diskHashes, this.registry).get(`${kind}:${name}`);
1285
1698
  if (onDisk !== void 0 && onDisk !== origin.sha256) {
1286
1699
  missing.push(
1287
- `${ref} (library "${origin.name}" content changed since lock \u2014 or the lock predates the one shared asset hash; re-lock with \`skaile install\`)`
1700
+ `${ref} (library "${lookup.name}" content changed since lock \u2014 or the lock predates the one shared asset hash; re-lock with \`skaile install\`)`
1288
1701
  );
1289
1702
  continue;
1290
1703
  }
1291
1704
  }
1705
+ found.version = current;
1292
1706
  entries.push(withLibraryFiles(found, libPath, fileSets, this.registry));
1293
1707
  }
1294
1708
  return { entries, missing };
1295
1709
  }
1296
1710
  /**
1297
- * On-disk root of the library named `name`: the registered path from the index
1298
- * when it exists, else `<libraries dir>/<name>`; `null` when neither is a directory.
1711
+ * Whether a deployed copy exists for the lock entries `install` kept for the unresolved
1712
+ * `ref`. A kind with no install target has no deployed copy.
1713
+ */
1714
+ keptCopyDeployed(kept, ref) {
1715
+ const want = parseRefTolerant(ref.split(" ")[0] ?? ref);
1716
+ if (!want) return false;
1717
+ return Object.keys(kept.assets).some((key) => {
1718
+ const id = parseCanonicalLockKey(key);
1719
+ if (id.kind !== want.kind || id.name !== want.name) return false;
1720
+ if (want.publisher && id.publisher !== want.publisher) return false;
1721
+ const component = this.installTarget(id.kind);
1722
+ return component !== void 0 && isDeployed(component, id.name, this.driverTarget, this.global, this.projectDir);
1723
+ });
1724
+ }
1725
+ /**
1726
+ * How the library `origin` names stands on this machine. THE one lookup of a locked library
1727
+ * ("is it registered here?"): `check`, `outdated`, `install` and `install --locked` all ask it.
1728
+ *
1729
+ * - registered in the index under its name: `ok` at its registered path, or `dir-missing` when
1730
+ * that directory is gone;
1731
+ * - not registered: a directory `<libraries dir>/<name>` nobody unregistered still counts as a
1732
+ * hand-made library (`ok`). One `library remove` unregistered without `--purge` does not
1733
+ * (it stays on disk but no longer resolves), and neither does the reserved `installed` folder.
1734
+ * - otherwise, when the lock recorded the library's remote URL: another registered library
1735
+ * whose git remote is the same repository (`.git` suffix, trailing `/` and the
1736
+ * `git@host:o/r` / `https://host/o/r` forms do not matter) and whose directory exists is
1737
+ * `ok`, with `name` set to the name it is registered under here. The library name is
1738
+ * machine-local; the remote is what a clone shares.
1299
1739
  */
1300
- libraryPath(name, snap) {
1301
- if (name === GLOBAL_INSTALL_DIR_NAME) return null;
1740
+ lockedLibrary(origin, snap) {
1741
+ const { name } = origin;
1742
+ if (name === GLOBAL_INSTALL_DIR_NAME) return { state: "unregistered" };
1743
+ let byName = { state: "unregistered" };
1302
1744
  const registered = snap?.libraries.find((l) => l.name === name)?.path;
1303
- if (registered && existsSync(registered)) return registered;
1304
- try {
1305
- const guess = join(resolveLibraryDir(), name);
1306
- return existsSync(guess) ? guess : null;
1307
- } catch {
1308
- return null;
1745
+ if (registered) {
1746
+ byName = existsSync(registered) ? { state: "ok", path: registered, name } : { state: "dir-missing", path: registered };
1747
+ } else {
1748
+ try {
1749
+ const guess = join(resolveLibraryDir(), name);
1750
+ if (existsSync(guess) && !unregisteredLibraryNames().has(name)) {
1751
+ byName = { state: "ok", path: guess, name };
1752
+ }
1753
+ } catch {
1754
+ }
1309
1755
  }
1756
+ if (byName.state === "unregistered") {
1757
+ const renamed = renamedLibraryName(name);
1758
+ const now = renamed ? snap?.libraries.find((l) => l.name === renamed) : void 0;
1759
+ if (now && existsSync(now.path)) return { state: "ok", path: now.path, name: now.name };
1760
+ }
1761
+ if (byName.state === "ok" || !origin.url) return byName;
1762
+ const wanted = normalizeRepoUrl(origin.url);
1763
+ const byUrl = snap?.libraries.find((l) => {
1764
+ const url = libraryRemoteUrl(l);
1765
+ return l.name !== name && url !== void 0 && normalizeRepoUrl(url) === wanted && existsSync(l.path);
1766
+ });
1767
+ return byUrl ? { state: "ok", path: byUrl.path, name: byUrl.name } : byName;
1768
+ }
1769
+ /** On-disk root of the library `origin` names; `null` unless {@link lockedLibrary} says `ok`. */
1770
+ libraryPath(origin, snap) {
1771
+ const found = this.lockedLibrary(origin, snap);
1772
+ return found.state === "ok" ? found.path : null;
1773
+ }
1774
+ /**
1775
+ * Where each library-origin lock entry of this project resolves on this machine: identity
1776
+ * `kind:publisher/name` (no scope sigil, no version) → the name the locked library is
1777
+ * registered under here. The name differs from the locked one when the lock's remote URL
1778
+ * matched another registered library ({@link lockedLibrary}). A deployed entry whose library
1779
+ * resolves nowhere (not registered, directory gone) maps to `null`.
1780
+ *
1781
+ * Direct `asset install` entries (`library.installed`) are included and resolved the same
1782
+ * way, but they map to the locked library name when that lookup fails: such a copy sits in
1783
+ * the project and does not depend on the library still being registered. A missing or
1784
+ * unreadable lock yields an empty map. `skaile manage` uses it to tick the right library row.
1785
+ */
1786
+ async lockedLibraryNames() {
1787
+ const out = /* @__PURE__ */ new Map();
1788
+ const locked = Object.entries(this.tryReadLock()?.assets ?? {}).filter(
1789
+ ([, e]) => e.library !== void 0
1790
+ );
1791
+ if (locked.length === 0) return out;
1792
+ const snap = await this.loadLibraryIndex();
1793
+ for (const [key, e] of locked) {
1794
+ const { publisher, kind, name } = parseCanonicalLockKey(key);
1795
+ if (!publisher || !kind || !name) continue;
1796
+ const origin = e.library;
1797
+ const lookup = this.lockedLibrary(origin, snap);
1798
+ const resolved = lookup.state === "ok" ? lookup.name : origin.installed ? origin.name : null;
1799
+ out.set(`${kind}:${publisher}/${name}`, resolved);
1800
+ }
1801
+ return out;
1310
1802
  }
1311
1803
  // ── Store fetch-then-deploy ────────────────────────────────────────────
1312
1804
  /** A candidate's stable identity key (`<publisher>/<kind>:<name>@<version>`). */
@@ -1443,7 +1935,10 @@ var AssetManager = class {
1443
1935
  return id !== null && (opts.provenanceIndex.get(id)?.length ?? 0) === 0;
1444
1936
  })
1445
1937
  );
1446
- const hit = await this.resolveLibraryEntries([...libraryTried]);
1938
+ const hit = await this.resolveLibraryEntries([...libraryTried], {
1939
+ quiet: !opts.warnCollisions,
1940
+ lockedLibraries: opts.lockedLibraries
1941
+ });
1447
1942
  const next = absorb(hit);
1448
1943
  const rest = fresh.filter((ref) => !hit.resolved.has(ref));
1449
1944
  if (rest.length > 0) {
@@ -1458,7 +1953,8 @@ var AssetManager = class {
1458
1953
  for (const o of r.overridesApplied) overridesApplied.add(o);
1459
1954
  presets.push(...r.presets);
1460
1955
  const hit2 = await this.resolveLibraryEntries(
1461
- r.missing.filter((m) => !libraryTried.has(m))
1956
+ r.missing.filter((m) => !libraryTried.has(m)),
1957
+ { quiet: !opts.warnCollisions, lockedLibraries: opts.lockedLibraries }
1462
1958
  );
1463
1959
  next.push(...absorb(hit2));
1464
1960
  for (const m of r.missing) if (!hit2.resolved.has(m)) missing.add(m);
@@ -1631,6 +2127,9 @@ var AssetManager = class {
1631
2127
  * @param opts.link - When `true`, symlink local-path assets instead of copying them
1632
2128
  * (the author dev-loop). Machine-local / not for committing; a no-op for
1633
2129
  * store/factory sources. Defaults to copy.
2130
+ * @param opts.moveLibraries - When `true` (`skaile update`), a locked library asset is
2131
+ * re-resolved from every library and may move to another one; {@link InstallResult.moved}
2132
+ * lists the moves. Without it a locked asset stays on the library its lock entry names.
1634
2133
  * @returns {@link InstallResult} summarising what was deployed, what was missing, and any collisions.
1635
2134
  */
1636
2135
  async install(opts) {
@@ -1638,6 +2137,20 @@ var AssetManager = class {
1638
2137
  if (opts?.locked) {
1639
2138
  return this.installLocked(repositories, patches, dependencies);
1640
2139
  }
2140
+ const lockForLibraries = this.tryReadLock();
2141
+ if (lockForLibraries) {
2142
+ const unavailable = await this.unavailableLockedLibraries(lockForLibraries, dependencies);
2143
+ if (unavailable.length > 0) {
2144
+ return {
2145
+ deployed: [],
2146
+ removed: [],
2147
+ missing: unavailable,
2148
+ lockWritten: false,
2149
+ npmInstalled: [],
2150
+ npmFailed: []
2151
+ };
2152
+ }
2153
+ }
1641
2154
  const cloneError = this.ensureSourceClones(repositories, sources);
1642
2155
  if (cloneError) return cloneError;
1643
2156
  const provenanceIndex = await buildProvenanceIndex(this.buildClones(repositories), {
@@ -1646,7 +2159,15 @@ var AssetManager = class {
1646
2159
  });
1647
2160
  const { resolved, missing, overridesApplied, lib, presets } = await this.resolveLibraryFirst(
1648
2161
  dependencies,
1649
- { provenanceIndex, overrides, stores, declaredRefs: dependencies }
2162
+ {
2163
+ provenanceIndex,
2164
+ overrides,
2165
+ stores,
2166
+ declaredRefs: dependencies,
2167
+ // `install` restores what the lock recorded: a changed default library or a second
2168
+ // library holding the same ref does not move a locked asset. `update` re-resolves.
2169
+ ...opts?.moveLibraries ? {} : { lockedLibraries: await this.lockedLibraryNames() }
2170
+ }
1650
2171
  );
1651
2172
  this.assertNoFolderCollision([...resolved, ...lib.entries.map((e) => this.entryIdentity(e))]);
1652
2173
  const stillMissing = missing.filter((m) => !lib.resolved.has(m));
@@ -1657,20 +2178,24 @@ var AssetManager = class {
1657
2178
  for (const ref of /* @__PURE__ */ new Set([...kept.refs, ...fixes.keys()])) {
1658
2179
  const fix = fixes.get(ref);
1659
2180
  const notes = [
1660
- kept.refs.has(ref) ? "the deployed copy and its lock entry are kept" : void 0,
2181
+ kept.refs.has(ref) ? this.keptCopyDeployed(kept, ref) ? "the deployed copy and its lock entry are kept" : "its lock entry is kept; no deployed copy exists" : void 0,
1661
2182
  fix
1662
2183
  ].filter((n) => n !== void 0);
1663
2184
  missingHints[ref] = `${fix ? "" : missingHints[ref] ?? ""} (${notes.join("; ")})`;
1664
2185
  }
1665
2186
  const directLock = this.directInstallLockEntries();
1666
2187
  const libraryLock = { ...directLock, ...await this.libraryLockEntries(lib) };
1667
- const freshLock = buildLockFile(resolved, overridesApplied, libraryLock);
2188
+ const freshLock = {
2189
+ ...buildLockFile(resolved, overridesApplied, libraryLock),
2190
+ ...presetsLockSlice(presets)
2191
+ };
1668
2192
  await this.keepRequiredByKept(prevLock, kept, Object.keys(freshLock.assets));
1669
- const removed = this.reconcileRemovedAssets(
2193
+ const { removed, replaced } = this.reconcileRemovedAssets(
1670
2194
  resolved,
1671
2195
  /* @__PURE__ */ new Set([...Object.keys(libraryLock), ...Object.keys(kept.assets)])
1672
2196
  );
1673
2197
  const entries = [...await this.buildCatalogEntries(resolved, repositories), ...lib.entries];
2198
+ const copyDrift = await this.lockedCopyDrift();
1674
2199
  const deployed = [
1675
2200
  ...new Set(
1676
2201
  this.targetHarnesses().flatMap(
@@ -1678,23 +2203,31 @@ var AssetManager = class {
1678
2203
  )
1679
2204
  )
1680
2205
  ];
2206
+ const restored = this.restoredCopies(copyDrift, deployed);
1681
2207
  await this.renderDriverMcpConfig();
1682
- const agentRender = this.renderAgentInstances();
1683
- const lockWritten = resolved.length > 0 || lib.entries.length > 0 || existsSync(this.lockPath);
2208
+ const agentRender = await this.renderAgentInstances();
2209
+ const lockWanted = resolved.length > 0 || lib.entries.length > 0 || existsSync(this.lockPath);
2210
+ let lockWritten = false;
1684
2211
  let relocked = [];
1685
- if (lockWritten) {
2212
+ let moved = [];
2213
+ if (lockWanted) {
1686
2214
  const lockData = withKeptLockEntries(freshLock, kept);
1687
2215
  relocked = relockedRefs(prevLock, lockData);
1688
- writeLock(this.lockPath, lockData);
2216
+ moved = libraryMoves(prevLock, libraryLock);
2217
+ lockWritten = writeLock(this.lockPath, lockData);
1689
2218
  }
1690
2219
  this.recordInstallHistory(deployed, removed);
1691
2220
  const { npmInstalled, npmFailed } = await this.installRuntimeAssetDeps();
2221
+ await this.adviseMissing(stillMissing, missingHints);
1692
2222
  return {
1693
2223
  deployed,
1694
2224
  removed,
1695
2225
  missing: stillMissing,
1696
2226
  ...Object.keys(missingHints).length > 0 ? { missingHints } : {},
1697
2227
  ...relocked.length > 0 ? { relocked } : {},
2228
+ ...moved.length > 0 ? { moved } : {},
2229
+ ...replaced.length > 0 ? { replaced } : {},
2230
+ ...restored.length > 0 ? { restored } : {},
1698
2231
  lockWritten,
1699
2232
  npmInstalled,
1700
2233
  npmFailed,
@@ -1714,7 +2247,7 @@ var AssetManager = class {
1714
2247
  const fresh = freshKeys.map(parseCanonicalLockKey);
1715
2248
  const queue = Object.keys(kept.assets);
1716
2249
  for (const key of queue) {
1717
- for (const dep of await this.deployedRequirements(key)) {
2250
+ for (const dep of await this.deployedRequirementsOf(parseCanonicalLockKey(key))) {
1718
2251
  const want = memberIdentity(dep);
1719
2252
  if (!want) continue;
1720
2253
  const matches = (id) => id.kind === want.kind && id.name === want.name && (!want.publisher || !id.publisher || id.publisher === want.publisher);
@@ -1730,9 +2263,14 @@ var AssetManager = class {
1730
2263
  }
1731
2264
  }
1732
2265
  }
1733
- /** Publisher-qualified refs the deployed copy of the asset behind lock `key` requires. */
1734
- async deployedRequirements(key) {
1735
- const { kind, name, publisher } = parseCanonicalLockKey(key);
2266
+ /**
2267
+ * Publisher-qualified refs the deployed copy of `id` requires, read from its own
2268
+ * manifest file (`metadata.requires`, a manifest `dependencies:` entry): the one place
2269
+ * the requirements stay knowable when no source or library holds the asset any more.
2270
+ * A copy that is not deployed or cannot be read has no known requirements.
2271
+ */
2272
+ async deployedRequirementsOf(id) {
2273
+ const { kind, name, publisher } = id;
1736
2274
  const component = kind ? this.installTarget(kind) : void 0;
1737
2275
  if (!kind || !name || !component) return [];
1738
2276
  const dir = deployedDir(component, name, this.driverTarget, this.global, this.projectDir);
@@ -1805,21 +2343,32 @@ var AssetManager = class {
1805
2343
  ...keep
1806
2344
  ]);
1807
2345
  const removed = [];
2346
+ const replaced = [];
1808
2347
  if (oldLock) {
1809
2348
  for (const ref of Object.keys(oldLock.assets)) {
1810
2349
  if (!resolvedRefs.has(ref)) {
1811
- const { kind, name } = parseCanonicalLockKey(ref);
2350
+ const { kind, name, publisher, version } = parseCanonicalLockKey(ref);
1812
2351
  if (kind === "preset") continue;
1813
2352
  if (kind && name) {
1814
2353
  for (const t of this.targetHarnesses()) removeAsset(kind, name, this.deployOptsFor(t));
1815
- removed.push(ref);
2354
+ const successor = [...resolvedRefs].find((r) => {
2355
+ const o = parseCanonicalLockKey(r);
2356
+ return o.kind === kind && o.name === name && o.publisher === publisher;
2357
+ });
2358
+ if (successor) {
2359
+ replaced.push({
2360
+ ref: `${kind}:${publisher ? `@${publisher}/` : ""}${name}`,
2361
+ from: version ?? "",
2362
+ to: parseCanonicalLockKey(successor).version ?? ""
2363
+ });
2364
+ } else removed.push(ref);
1816
2365
  } else {
1817
2366
  console.warn(`skaile: cannot reconcile unparseable lock key "${ref}" \u2014 left in place.`);
1818
2367
  }
1819
2368
  }
1820
2369
  }
1821
2370
  }
1822
- return removed;
2371
+ return { removed, replaced };
1823
2372
  }
1824
2373
  /** Append `add`/`remove` history entries for a completed install reconcile. */
1825
2374
  recordInstallHistory(deployed, removed) {
@@ -1935,9 +2484,39 @@ var AssetManager = class {
1935
2484
  }
1936
2485
  return { npmInstalled, npmFailed };
1937
2486
  }
2487
+ /**
2488
+ * Give every ref `install` could not resolve, and that has no hint yet, the fix the shared
2489
+ * {@link AssetManager.projectState} names for it (a declared ref no library or source holds,
2490
+ * a requirement nothing holds). Runs after the deploy, so a requirer is known by then.
2491
+ * Mutates `hints`: ` <reason>` follows the ref in the `Missing:` line.
2492
+ */
2493
+ async adviseMissing(refs, hints) {
2494
+ const open = refs.filter((r) => !hints[r]);
2495
+ if (open.length === 0) return;
2496
+ const { gaps } = await this.projectState();
2497
+ for (const ref of open) {
2498
+ const want = parseRefTolerant(ref.split(" ")[0] ?? ref);
2499
+ if (!want) continue;
2500
+ const gap = gaps.find(
2501
+ (g) => (g.type === "declared" || g.type === "requires") && g.manualFix && sameAsset(g.ref, want)
2502
+ );
2503
+ if (gap) hints[ref] = ` ${gap.reason}`;
2504
+ }
2505
+ }
1938
2506
  async installLocked(repositories, patches, dependencies) {
1939
2507
  const lock = this.tryReadLock();
1940
2508
  if (!lock) {
2509
+ const gaps2 = (await this.projectState()).gaps.filter((g) => g.type !== "agent");
2510
+ if (gaps2.some((g) => g.manualFix)) {
2511
+ return {
2512
+ deployed: [],
2513
+ removed: [],
2514
+ missing: gaps2.map((g) => g.message),
2515
+ lockWritten: false,
2516
+ npmInstalled: [],
2517
+ npmFailed: []
2518
+ };
2519
+ }
1941
2520
  throw new Error("No lock file found. Run `skaile install` first.");
1942
2521
  }
1943
2522
  this.assertNoFolderCollision(Object.keys(lock.assets).map(parseCanonicalLockKey));
@@ -1999,6 +2578,7 @@ var AssetManager = class {
1999
2578
  if (!repositories[slug]) repositories[slug] = { url: entry.source.url, path: cloneDir };
2000
2579
  }
2001
2580
  const library = await this.restoreLibraryLockEntries(lock);
2581
+ const copyDrift = await this.lockedCopyDrift();
2002
2582
  const deployed = [
2003
2583
  ...new Set(
2004
2584
  this.targetHarnesses().flatMap(
@@ -2011,15 +2591,17 @@ var AssetManager = class {
2011
2591
  )
2012
2592
  )
2013
2593
  ];
2594
+ const restored = this.restoredCopies(copyDrift, deployed);
2014
2595
  await this.renderDriverMcpConfig();
2015
- const agentRender = this.renderAgentInstances();
2596
+ const agentRender = await this.renderAgentInstances();
2016
2597
  const alreadyMissing = new Set(
2017
2598
  library.missing.map((m) => {
2018
2599
  const p = parseRefTolerant(m.split(" ")[0] ?? m);
2019
2600
  return p ? `${p.kind}:${p.name}` : m;
2020
2601
  })
2021
2602
  );
2022
- const gaps = (await this.lockGaps()).filter((g) => {
2603
+ const gaps = (await this.projectState()).gaps.filter((g) => {
2604
+ if (g.type === "agent") return false;
2023
2605
  const p = parseRefTolerant(g.ref);
2024
2606
  return !(p && alreadyMissing.has(`${p.kind}:${p.name}`));
2025
2607
  }).map((g) => g.message);
@@ -2028,9 +2610,68 @@ var AssetManager = class {
2028
2610
  removed: [],
2029
2611
  missing: [...library.missing, ...gaps],
2030
2612
  lockWritten: false,
2613
+ ...restored.length > 0 ? { restored } : {},
2031
2614
  ...agentRender
2032
2615
  };
2033
2616
  }
2617
+ /**
2618
+ * Parse the ref `add` was given. A ref the grammar rejects fails with the parser's message
2619
+ * minus its manifest-migration advice (it points at a skill that is not for `add`), and:
2620
+ * - a publisher-less ref (`report-html`, `skill:report-html`) names the assets of that
2621
+ * kind and name that `add` can resolve (a source, the bundled factory source or a
2622
+ * library holds them), so `Did you mean` never names an asset that does not resolve;
2623
+ * - a `@version` suffix (`skill:@acme/x@1.0.0`) says the pin is written `#<version>`.
2624
+ */
2625
+ async parseAddRef(ref, repositories, sources) {
2626
+ try {
2627
+ return parseAssetRef(ref);
2628
+ } catch (err) {
2629
+ if (!(err instanceof Error)) throw err;
2630
+ const advice = " Run the `migrate-skaile-manifest` skill on legacy manifests.";
2631
+ const plain = err.message.endsWith(advice) ? err.message.slice(0, -advice.length) : err.message;
2632
+ if (plain.startsWith("publisher required")) {
2633
+ const matches = await this.resolvableMatches(ref, repositories, sources);
2634
+ const hint = matches.length === 0 ? "" : matches.length === 1 ? ` Did you mean ${matches[0]}?` : ` Did you mean one of: ${matches.join(", ")}?`;
2635
+ throw new Error(`${plain}${hint}`);
2636
+ }
2637
+ const pinned = /^(.*\/[^@#/]+)@([^@#/]+)$/.exec(ref.trim());
2638
+ if (pinned && plain.startsWith("invalid asset name")) {
2639
+ throw new Error(`${plain} To pin a version, write ${pinned[1]}#${pinned[2]}.`);
2640
+ }
2641
+ throw new Error(plain);
2642
+ }
2643
+ }
2644
+ /**
2645
+ * The publisher-qualified refs of the assets named like the publisher-less `ref` that
2646
+ * `add` resolves: held by a source clone, the bundled factory source or a registered
2647
+ * library. A ref without a kind means a skill, as the grammar says.
2648
+ */
2649
+ async resolvableMatches(ref, repositories, sources) {
2650
+ const trimmed = ref.trim();
2651
+ const colon = trimmed.indexOf(":");
2652
+ const rawKind = colon === -1 ? "skill" : trimmed.slice(0, colon).trim();
2653
+ const kind = rawKind === "mcp" ? "mcp-server" : rawKind;
2654
+ const hash = trimmed.indexOf("#", colon + 1);
2655
+ const name = trimmed.slice(colon + 1, hash === -1 ? void 0 : hash).trim();
2656
+ if (!name || /[@/\s]/.test(name)) return [];
2657
+ const found = /* @__PURE__ */ new Set();
2658
+ const note = (c) => {
2659
+ if (c.kind === kind && c.name === name && c.publisher) {
2660
+ found.add(assetRefToStr({ kind, name, publisher: c.publisher.replace(/^@/, "") }));
2661
+ }
2662
+ };
2663
+ try {
2664
+ this.ensureSourceClonesQuiet(repositories, sources);
2665
+ const index = await buildProvenanceIndex(this.buildClones(repositories), {
2666
+ projectDir: this.projectDir,
2667
+ knownKinds: this.knownKinds
2668
+ });
2669
+ for (const candidates of index.values()) for (const c of candidates) note(c);
2670
+ for (const { entry } of await this.libraryAssets()) note(entry);
2671
+ } catch {
2672
+ }
2673
+ return [...found].sort();
2674
+ }
2034
2675
  /**
2035
2676
  * Add a single asset (and its transitive dependencies) to the workspace.
2036
2677
  *
@@ -2044,6 +2685,8 @@ var AssetManager = class {
2044
2685
  * store/factory sources. Defaults to copy.
2045
2686
  * @param opts.onLibraryOrigin - Called once per deployed library-origin asset with its
2046
2687
  * canonical ref and the name of the library it was resolved from.
2688
+ * @param opts.onLibraryMoved - Called once per asset whose lock entry named another library
2689
+ * than the one this add resolved it from.
2047
2690
  * @param opts.onPresets - Called once with the presets expanded into this add (the
2048
2691
  * added preset and any it nests); a library-held preset names its library.
2049
2692
  * @returns Array of asset refs that were deployed in this call.
@@ -2052,7 +2695,7 @@ var AssetManager = class {
2052
2695
  */
2053
2696
  async add(ref, opts) {
2054
2697
  const { repositories, sources, stores, dependencies, overrides, patches } = this.loadConfig();
2055
- const parsed = parseAssetRef(ref);
2698
+ const parsed = await this.parseAddRef(ref, repositories, sources);
2056
2699
  this.ensureSourceClonesQuiet(repositories, sources);
2057
2700
  const provenanceIndex = await buildProvenanceIndex(this.buildClones(repositories), {
2058
2701
  projectDir: this.projectDir,
@@ -2060,9 +2703,24 @@ var AssetManager = class {
2060
2703
  });
2061
2704
  const { resolved, missing, overridesApplied, lib, presets } = await this.resolveLibraryFirst(
2062
2705
  [ref],
2063
- { provenanceIndex, overrides, stores, declaredRefs: dependencies }
2706
+ { provenanceIndex, overrides, stores, declaredRefs: dependencies, warnCollisions: true }
2064
2707
  );
2065
2708
  if (resolved.length === 0 && lib.entries.length === 0 && presets.length === 0) {
2709
+ const snap = await this.loadLibraryIndex();
2710
+ const bare = (p) => (p ?? "").replace(/^@/, "");
2711
+ for (const l of snap?.libraries ?? []) {
2712
+ const libPath = this.libraryPath({ name: l.name }, snap);
2713
+ if (!libPath || !manifestFileHasAssetsArray(libPath)) continue;
2714
+ const unlisted = await scanUnlistedAssets(libPath, l.name, this.registry);
2715
+ const hit = unlisted.find(
2716
+ (u) => !u.registeredAt && u.entry.kind === parsed.kind && u.entry.name === parsed.name && (!parsed.publisher || bare(u.entry.publisher) === bare(parsed.publisher))
2717
+ );
2718
+ if (hit) {
2719
+ throw new Error(
2720
+ `${canonicalEntryRef(hit.entry)} is in library "${l.name}" but not listed in its skaile.manifest.yaml, so only part of its files would be deployed. Run \`skaile library index ${l.name}\`, then add it again.`
2721
+ );
2722
+ }
2723
+ }
2066
2724
  throw new Error(
2067
2725
  `Asset not found: ${ref}${(await this.publisherChangeHints([ref]))[ref] ?? ""}${missing.length ? ` (missing: ${missing.join(", ")})` : ""}`
2068
2726
  );
@@ -2072,6 +2730,18 @@ var AssetManager = class {
2072
2730
  `Preset ${ref} has items that do not resolve: ${missing.join(", ")} \u2014 nothing was added`
2073
2731
  );
2074
2732
  }
2733
+ if (lib.entries.length > 0) {
2734
+ const snap = await this.loadLibraryIndex();
2735
+ for (const e of lib.entries) {
2736
+ const origin = lib.libraryOf.get(e);
2737
+ const libPath = origin ? this.libraryPath({ name: origin }, snap) : null;
2738
+ if (origin && libPath && isUnlistedLibraryAsset(e, libPath)) {
2739
+ throw new Error(
2740
+ `${canonicalEntryRef(e)} is in library "${origin}" but not listed in its skaile.manifest.yaml, so only part of its files would be deployed. Run \`skaile library index ${origin}\`, then add it again.`
2741
+ );
2742
+ }
2743
+ }
2744
+ }
2075
2745
  this.assertNoFolderCollision(
2076
2746
  [...resolved, ...lib.entries.map((e) => this.entryIdentity(e))],
2077
2747
  [
@@ -2080,15 +2750,20 @@ var AssetManager = class {
2080
2750
  ]
2081
2751
  );
2082
2752
  if (missing.length > 0) {
2083
- console.warn(
2084
- `skaile: ${ref} was added without unresolved dependencies: ${missing.join(", ")}`
2753
+ throw new Error(
2754
+ `${ref.split("#")[0]} requires ${missing.join(", ")} (not found \u2014 ${REGISTER_HOLDER}, or remove the requirement from the asset's manifest) \u2014 nothing was added`
2085
2755
  );
2086
2756
  }
2087
2757
  const entries = [...await this.buildCatalogEntries(resolved, repositories), ...lib.entries];
2088
2758
  const deployed = [
2089
2759
  ...new Set(
2090
2760
  this.targetHarnesses().flatMap(
2091
- (t) => deployAll(entries, repositories, patches, this.deployOptsFor(t, opts?.link))
2761
+ (t) => deployAll(
2762
+ entries,
2763
+ repositories,
2764
+ patches,
2765
+ this.deployOptsFor(t, opts?.link, opts?.onSkippedSymlink, opts?.onEnvFile)
2766
+ )
2092
2767
  )
2093
2768
  )
2094
2769
  ];
@@ -2102,10 +2777,18 @@ var AssetManager = class {
2102
2777
  if (presets.length > 0) opts?.onPresets?.(presets);
2103
2778
  this.addDependencyToConfig(ref);
2104
2779
  await this.renderDriverMcpConfig();
2105
- this.renderAgentInstances();
2780
+ await this.renderAgentInstances();
2106
2781
  if (resolved.length > 0 || lib.entries.length > 0) {
2107
- const fresh = buildLockFile(resolved, overridesApplied, await this.libraryLockEntries(lib));
2108
- writeLock(this.lockPath, mergeLockFile(this.tryReadLock(), fresh));
2782
+ const libraryLock = await this.libraryLockEntries(lib);
2783
+ const fresh = {
2784
+ ...buildLockFile(resolved, overridesApplied, libraryLock),
2785
+ ...presetsLockSlice(presets)
2786
+ };
2787
+ const prevLock = this.tryReadLock();
2788
+ if (opts?.onLibraryMoved) {
2789
+ for (const m of libraryMoves(prevLock, libraryLock)) opts.onLibraryMoved(m);
2790
+ }
2791
+ writeLock(this.lockPath, mergeLockFile(prevLock, fresh));
2109
2792
  }
2110
2793
  for (const d of deployed) {
2111
2794
  appendHistory(this.projectDir, {
@@ -2163,16 +2846,16 @@ var AssetManager = class {
2163
2846
  const target = memberIdentity(ref);
2164
2847
  if (!target || target.kind === "preset") return [];
2165
2848
  try {
2166
- const sameAsset = (id, want) => !!id && id.kind === want.kind && id.name === want.name && (!id.publisher || !want.publisher || id.publisher === want.publisher);
2849
+ const sameAsset2 = (id, want) => !!id && id.kind === want.kind && id.name === want.name && (!id.publisher || !want.publisher || id.publisher === want.publisher);
2167
2850
  const declared = this.loadConfig().dependencies.map(memberIdentity);
2168
2851
  const out = [];
2169
2852
  const seen = /* @__PURE__ */ new Set();
2170
2853
  for (const p of await this.search(void 0, "preset")) {
2171
2854
  const members = [...p.dependencies, ...p.requires.map(depToStr)];
2172
- if (!members.some((m) => sameAsset(memberIdentity(m), target))) continue;
2855
+ if (!members.some((m) => sameAsset2(memberIdentity(m), target))) continue;
2173
2856
  const publisher = p.publisher?.replace(/^@/, "");
2174
2857
  const presetId = { kind: "preset", name: p.name, publisher };
2175
- if (declared.some((d) => sameAsset(d, presetId))) continue;
2858
+ if (declared.some((d) => sameAsset2(d, presetId))) continue;
2176
2859
  const presetRef = publisher ? assetRefToStr({ kind: "preset", name: p.name, publisher }) : `preset:${p.name}`;
2177
2860
  if (seen.has(presetRef)) continue;
2178
2861
  seen.add(presetRef);
@@ -2383,19 +3066,39 @@ var AssetManager = class {
2383
3066
  * preset's items count as its requirements. They are undeployed and unlocked;
2384
3067
  * a dependency still declared or required elsewhere stays.
2385
3068
  *
3069
+ * An asset another locked asset (or a declared preset) requires is kept and reported in
3070
+ * `blocked`, nothing changes, unless `opts.force` (see {@link requiredByOthers}).
3071
+ *
2386
3072
  * @returns `changed` as {@link remove}; `pruned` the canonical refs
2387
- * (`kind:@publisher/name`) of the dependencies dropped as well.
3073
+ * (`kind:@publisher/name`) of the dependencies dropped as well; `blocked` when the
3074
+ * removal was refused.
2388
3075
  */
2389
- async removeWithDependencies(ref) {
3076
+ async removeWithDependencies(ref, opts) {
2390
3077
  const target = this.resolveRemoveTarget(ref);
2391
3078
  if (!target) return { changed: false, pruned: [] };
3079
+ if (!opts?.force) {
3080
+ const blocked = await this.requiredByOthers(target);
3081
+ if (blocked) return { changed: false, pruned: [], blocked };
3082
+ }
2392
3083
  const asked = target.publisher ? assetRefToStr({ kind: target.kind, name: target.name, publisher: target.publisher }) : `${target.kind}:${target.name}`;
2393
3084
  let entry = null;
2394
3085
  try {
2395
3086
  entry = await this.info(asked);
2396
3087
  } catch {
2397
3088
  }
2398
- const requiredRefs = entry ? libraryEntryDepRefs(entry) : [];
3089
+ let requiredRefs;
3090
+ if (entry) {
3091
+ requiredRefs = libraryEntryDepRefs(entry);
3092
+ } else {
3093
+ const lockedAs = Object.keys(this.tryReadLock()?.assets ?? {}).map(parseCanonicalLockKey).find(
3094
+ (id) => id.kind === target.kind && id.name === target.name && (!target.publisher || id.publisher === target.publisher)
3095
+ );
3096
+ requiredRefs = await this.deployedRequirementsOf({
3097
+ kind: target.kind,
3098
+ name: target.name,
3099
+ publisher: target.publisher ?? lockedAs?.publisher
3100
+ });
3101
+ }
2399
3102
  const changed = this.removeResolved(target);
2400
3103
  if (!changed || requiredRefs.length === 0) return { changed, pruned: [] };
2401
3104
  return { changed, pruned: await this.pruneOrphans(requiredRefs) };
@@ -2449,15 +3152,65 @@ var AssetManager = class {
2449
3152
  return kind ? { kind, name } : null;
2450
3153
  }
2451
3154
  }
3155
+ /**
3156
+ * The reverse guard of {@link removeWithDependencies}: which other locked assets (and
3157
+ * declared presets) require `target`. Their requirements are read the way {@link lockedGraph}
3158
+ * reads them for pruning. Never touches the project.
3159
+ *
3160
+ * @returns `ref` and `requiredBy` (both `kind:@publisher/name`, version-agnostic), or `null`
3161
+ * when `target` is not locked or nothing else requires it.
3162
+ */
3163
+ async requiredByOthers(target) {
3164
+ const lock = this.tryReadLock();
3165
+ if (!lock) return null;
3166
+ const { requires, findKey } = await this.lockedGraph(lock);
3167
+ const targetKey = findKey(
3168
+ target.publisher ? assetRefToStr({ kind: target.kind, name: target.name, publisher: target.publisher }) : `${target.kind}:${target.name}`
3169
+ );
3170
+ if (!targetKey) return null;
3171
+ const display = (node) => {
3172
+ if (!lock.assets[node]) return node;
3173
+ const { kind, name, publisher } = parseCanonicalLockKey(node);
3174
+ if (!kind || !name) return node;
3175
+ return publisher ? assetRefToStr({ kind, name, publisher }) : `${kind}:${name}`;
3176
+ };
3177
+ const requiredBy = [...requires].filter(([node, deps]) => node !== targetKey && deps.includes(targetKey)).map(([node]) => display(node));
3178
+ return requiredBy.length > 0 ? { ref: display(targetKey), requiredBy } : null;
3179
+ }
3180
+ /**
3181
+ * Publishers under which the lock or `skaile.yaml` track `kind:name`. A deployed dir carries
3182
+ * no publisher, so this is what says whose copy it is. `undefined` stands for a
3183
+ * publisher-less declaration.
3184
+ */
3185
+ trackedPublishers(kind, name) {
3186
+ const found = /* @__PURE__ */ new Set();
3187
+ const note = (id) => {
3188
+ if (id?.kind === kind && id.name === name) found.add(id.publisher?.replace(/^@/, ""));
3189
+ };
3190
+ for (const key of Object.keys(this.tryReadLock()?.assets ?? {})) {
3191
+ note(parseCanonicalLockKey(key));
3192
+ }
3193
+ try {
3194
+ for (const dep of this.loadConfig().dependencies) note(memberIdentity(dep));
3195
+ } catch {
3196
+ }
3197
+ return found;
3198
+ }
2452
3199
  /** Undeploy `parsed` from every target harness and drop its declaration and lock entry. */
2453
3200
  removeResolved(parsed) {
3201
+ const wantPub = parsed.publisher?.replace(/^@/, "");
3202
+ const tracked = this.trackedPublishers(parsed.kind, parsed.name);
3203
+ const someoneElses = wantPub !== void 0 && tracked.size > 0 && !tracked.has(wantPub) && !tracked.has(void 0);
2454
3204
  let undeployed = false;
2455
- for (const t of this.targetHarnesses()) {
2456
- if (removeAsset(parsed.kind, parsed.name, this.deployOptsFor(t))) undeployed = true;
3205
+ if (!someoneElses) {
3206
+ for (const t of this.targetHarnesses()) {
3207
+ if (removeAsset(parsed.kind, parsed.name, this.deployOptsFor(t))) undeployed = true;
3208
+ }
2457
3209
  }
2458
3210
  const undeclared = this.removeDependencyFromConfig(parsed);
2459
- const unlocked = undeployed || undeclared ? this.pruneLockEntry(parsed) : false;
2460
- const changed = undeployed || undeclared || unlocked;
3211
+ const unbound = this.removeAgentBindings(parsed);
3212
+ const unlocked = undeployed || undeclared || unbound ? this.pruneLockEntry(parsed) : false;
3213
+ const changed = undeployed || undeclared || unbound || unlocked;
2461
3214
  if (changed) {
2462
3215
  appendHistory(this.projectDir, {
2463
3216
  ref: `${parsed.kind}:${parsed.name}`,
@@ -2518,6 +3271,17 @@ var AssetManager = class {
2518
3271
  changed = true;
2519
3272
  }
2520
3273
  }
3274
+ if (parsed.kind === "preset" && lock.presets) {
3275
+ const kept = lock.presets.filter((key) => {
3276
+ const id = parseCanonicalLockKey(key);
3277
+ return !(id.kind === parsed.kind && id.name === parsed.name && (!parsed.publisher || id.publisher === parsed.publisher));
3278
+ });
3279
+ if (kept.length !== lock.presets.length) {
3280
+ if (kept.length > 0) lock.presets = kept;
3281
+ else delete lock.presets;
3282
+ changed = true;
3283
+ }
3284
+ }
2521
3285
  if (changed) writeLock(this.lockPath, lock);
2522
3286
  return changed;
2523
3287
  }
@@ -2578,6 +3342,7 @@ var AssetManager = class {
2578
3342
  const { repositories } = this.loadConfig();
2579
3343
  const parsed = parseRefTolerant(ref);
2580
3344
  if (!parsed) return null;
3345
+ let best;
2581
3346
  for (const [name, decl] of Object.entries(repositories)) {
2582
3347
  const repoDir = this.resolveRepoDir(decl, name);
2583
3348
  if (!repoDir) continue;
@@ -2585,9 +3350,12 @@ var AssetManager = class {
2585
3350
  const match = entries.find(
2586
3351
  (e) => e.kind === parsed.kind && e.name === parsed.name && (!parsed.publisher || e.publisher === parsed.publisher)
2587
3352
  );
2588
- if (match) return match;
3353
+ if (match && (!best || gt(coerceVersion(match.version ?? ""), coerceVersion(best.version ?? "")))) {
3354
+ best = match;
3355
+ }
2589
3356
  }
2590
- const lib = await this.resolveLibraryEntries([ref]);
3357
+ if (best) return best;
3358
+ const lib = await this.resolveLibraryEntries([ref], { quiet: true });
2591
3359
  return lib.entries[0] ?? null;
2592
3360
  }
2593
3361
  /**
@@ -2929,7 +3697,8 @@ var AssetManager = class {
2929
3697
  const out = [];
2930
3698
  try {
2931
3699
  const entry = await this.info(ref);
2932
- for (const dep of entry ? libraryEntryDepRefs(entry) : []) {
3700
+ const deps = entry ? libraryEntryDepRefs(entry) : lock.assets[node] ? await this.deployedRequirementsOf(parseCanonicalLockKey(node)) : [];
3701
+ for (const dep of deps) {
2933
3702
  const key = findKey(dep);
2934
3703
  if (key && key !== node && !out.includes(key)) out.push(key);
2935
3704
  }
@@ -3022,7 +3791,7 @@ var AssetManager = class {
3022
3791
  return locked.kind === asked.kind && locked.name === asked.name;
3023
3792
  };
3024
3793
  const key = lock.assets[ref] ? ref : Object.keys(lock.assets).find(matchesIdentity);
3025
- if (!key) return [];
3794
+ if (!key) return asked?.kind === "preset" ? this.whyPreset(lock, asked) : [];
3026
3795
  const { requires, roots } = await this.lockedGraph(lock);
3027
3796
  if (roots.includes(key)) return [key, "direct (skaile.yaml)"];
3028
3797
  const requirers = /* @__PURE__ */ new Map();
@@ -3047,6 +3816,52 @@ var AssetManager = class {
3047
3816
  }
3048
3817
  return [key, "not required by a declared dependency"];
3049
3818
  }
3819
+ /**
3820
+ * The {@link AssetManager.why} chain of a preset. A preset is never a lock asset: its record
3821
+ * is the lock's `presets` slice. Declared in `skaile.yaml` it is `direct`; one only a declared
3822
+ * preset nests is traced through the presets that list it.
3823
+ *
3824
+ * @returns `[]` when the lock does not record the preset.
3825
+ */
3826
+ async whyPreset(lock, asked) {
3827
+ const wanted = memberIdentity(assetRefToStr(asked));
3828
+ const same = (ref) => {
3829
+ const id = memberIdentity(ref);
3830
+ return id !== null && wanted !== null && id.kind === "preset" && id.name === wanted.name && (!id.publisher || !wanted.publisher || id.publisher === wanted.publisher);
3831
+ };
3832
+ const recorded = (lock.presets ?? []).find(same);
3833
+ if (!recorded) return [];
3834
+ let declared = [];
3835
+ try {
3836
+ declared = this.loadConfig().dependencies;
3837
+ } catch {
3838
+ }
3839
+ const roots = declared.filter((d) => memberIdentity(d)?.kind === "preset");
3840
+ if (roots.some(same)) return [recorded, "direct (skaile.yaml)"];
3841
+ const parent = /* @__PURE__ */ new Map();
3842
+ const seen = new Set(roots);
3843
+ const queue = [...roots];
3844
+ for (let node = queue.shift(); node !== void 0; node = queue.shift()) {
3845
+ let items = [];
3846
+ try {
3847
+ const entry = await this.info(node);
3848
+ items = entry ? libraryEntryDepRefs(entry) : [];
3849
+ } catch {
3850
+ }
3851
+ for (const item of items) {
3852
+ if (memberIdentity(item)?.kind !== "preset" || seen.has(item)) continue;
3853
+ seen.add(item);
3854
+ parent.set(item, node);
3855
+ if (same(item)) {
3856
+ const chain = [];
3857
+ for (let c = item; c !== void 0; c = parent.get(c)) chain.push(c);
3858
+ return [...chain, "direct (skaile.yaml)"];
3859
+ }
3860
+ queue.push(item);
3861
+ }
3862
+ }
3863
+ return [recorded, "not required by a declared dependency"];
3864
+ }
3050
3865
  /**
3051
3866
  * List installed assets that have a newer upstream available.
3052
3867
  *
@@ -3057,11 +3872,12 @@ var AssetManager = class {
3057
3872
  * - **Library assets** — a library-origin lock entry whose library files on disk
3058
3873
  * (not the index, which lags until `skaile library index`) hold a newer version
3059
3874
  * than the locked one (`latestVersion`, `library` set), or the same version with
3060
- * different bytes (`contentChanged`). `behind` is 0. An unregistered library is a
3061
- * `problem` row.
3062
- * - **Lock gaps** — every {@link AssetManager.lockGaps} gap (a declared dependency or a
3063
- * deployed asset's `requires` with no lock entry, a lock that contradicts a
3064
- * `skaile.yaml` pin) as a `problem` row, so `outdated` agrees with `check`.
3875
+ * different bytes (`contentChanged`). `behind` is 0.
3876
+ * - **Project state** — every {@link AssetManager.projectState} gap (an unregistered or
3877
+ * renamed library, a missing deployed copy, a declared dependency or a deployed asset's
3878
+ * `requires` with no lock entry, a lock that contradicts a `skaile.yaml` pin) as a
3879
+ * `problem` row, so `outdated` lists what `check`, `doctor` and `install --locked` fail
3880
+ * on. An asset that also has a version row is listed once, with that row.
3065
3881
  *
3066
3882
  * @returns Array of {@link OutdatedEntry} objects.
3067
3883
  */
@@ -3070,7 +3886,6 @@ var AssetManager = class {
3070
3886
  const lock = this.tryReadLock();
3071
3887
  if (!lock) return this.addLockGapRows([]);
3072
3888
  const result = [];
3073
- const reported = /* @__PURE__ */ new Set();
3074
3889
  const commitCounts = /* @__PURE__ */ new Map();
3075
3890
  for (const src of lock.sources) {
3076
3891
  const slug = sourceCacheKey(src.url);
@@ -3094,7 +3909,6 @@ var AssetManager = class {
3094
3909
  currentVersion: version ?? "",
3095
3910
  behind
3096
3911
  });
3097
- reported.add(ref);
3098
3912
  }
3099
3913
  }
3100
3914
  const libraryAssets = Object.entries(lock.assets).filter(
@@ -3108,28 +3922,9 @@ var AssetManager = class {
3108
3922
  const origin = entry.library;
3109
3923
  const { publisher, kind, name } = parseCanonicalLockKey(ref);
3110
3924
  if (!publisher || !kind || !name) continue;
3111
- const libPath = this.libraryPath(origin.name, snap);
3112
- if (!libPath) {
3113
- const canonical = `${kind}:@${publisher}/${name}`;
3114
- const heldElsewhere = (await this.resolveLibraryEntries([canonical], { quiet: true })).resolved.size > 0;
3115
- const fix = heldElsewhere ? void 0 : (await this.unresolvedFixes([canonical])).get(canonical) ?? unregisteredLibraryFix(origin.name, canonical);
3116
- result.push({
3117
- kind,
3118
- name,
3119
- publisher,
3120
- currentVersion: origin.version,
3121
- behind: 0,
3122
- library: origin.name,
3123
- ...fix === void 0 ? {
3124
- problem: `library "${origin.name}" is not registered (renamed?) \u2014 run \`skaile install\` to re-lock it against the library that holds it now`
3125
- } : {
3126
- problem: `library "${origin.name}" is not registered (renamed or removed?) \u2014 ${fix}`,
3127
- manualFix: true
3128
- }
3129
- });
3130
- reported.add(ref);
3131
- continue;
3132
- }
3925
+ const lookup = this.lockedLibrary(origin, snap);
3926
+ if (lookup.state !== "ok") continue;
3927
+ const libPath = lookup.path;
3133
3928
  const scanKey = `${libPath}\0${publisher}`;
3134
3929
  let scanned = scans.get(scanKey);
3135
3930
  if (!scanned) {
@@ -3138,7 +3933,7 @@ var AssetManager = class {
3138
3933
  }
3139
3934
  const found = scanned.find((x) => x.kind === kind && x.name === name);
3140
3935
  if (!found) continue;
3141
- const diskVersion = found.version || newestLibraryDef(snap, origin.name, { kind, publisher, name })?.version;
3936
+ const diskVersion = found.version || newestLibraryDef(snap, lookup.name, { kind, publisher, name })?.version;
3142
3937
  if (diskVersion && gt(coerceVersion(diskVersion), coerceVersion(origin.version))) {
3143
3938
  result.push({
3144
3939
  kind,
@@ -3147,9 +3942,8 @@ var AssetManager = class {
3147
3942
  currentVersion: origin.version,
3148
3943
  behind: 0,
3149
3944
  latestVersion: diskVersion,
3150
- library: origin.name
3945
+ library: lookup.name
3151
3946
  });
3152
- reported.add(ref);
3153
3947
  continue;
3154
3948
  }
3155
3949
  if (origin.sha256) {
@@ -3163,41 +3957,24 @@ var AssetManager = class {
3163
3957
  publisher,
3164
3958
  currentVersion: origin.version,
3165
3959
  behind: 0,
3166
- library: origin.name,
3960
+ library: lookup.name,
3167
3961
  contentChanged: true
3168
3962
  });
3169
- reported.add(ref);
3170
3963
  }
3171
3964
  }
3172
3965
  }
3173
3966
  }
3174
- for (const [ref, entry] of Object.entries(lock.assets)) {
3175
- if (reported.has(ref) || entry.library?.installed) continue;
3176
- const { publisher, kind, name, version } = parseCanonicalLockKey(ref);
3177
- const component = kind ? this.installTarget(kind) : void 0;
3178
- if (!kind || !name || !component) continue;
3179
- if (isDeployed(component, name, this.driverTarget, this.global, this.projectDir)) {
3180
- continue;
3181
- }
3182
- result.push({
3183
- kind,
3184
- name,
3185
- publisher: publisher ?? "",
3186
- currentVersion: entry.library?.version ?? version ?? "",
3187
- behind: 0,
3188
- ...entry.library ? { library: entry.library.name } : {},
3189
- problem: "deployed copy missing"
3190
- });
3191
- }
3192
3967
  return this.addLockGapRows(result);
3193
3968
  }
3194
3969
  /**
3195
- * Append one `problem` row per {@link AssetManager.lockGaps} gap whose asset is not
3196
- * listed yet, so `outdated` reports the same set `check` and `install --locked` fail on.
3970
+ * Add one `problem` row per {@link AssetManager.projectState} gap whose asset is not
3971
+ * listed yet, so `outdated` lists the same assets `check`, `doctor` and `install --locked`
3972
+ * fail on. An asset that is behind its source and also broken is listed once, with its
3973
+ * version row.
3197
3974
  */
3198
3975
  async addLockGapRows(rows) {
3199
3976
  const listed = new Set(rows.map((r) => `${r.kind}:${r.name}`));
3200
- for (const gap of await this.lockGaps()) {
3977
+ for (const gap of (await this.projectState()).gaps) {
3201
3978
  const parsed = parseRefTolerant(gap.ref);
3202
3979
  const id = parsed ? `${parsed.kind}:${parsed.name}` : gap.ref;
3203
3980
  if (listed.has(id)) continue;
@@ -3214,6 +3991,33 @@ var AssetManager = class {
3214
3991
  }
3215
3992
  return rows;
3216
3993
  }
3994
+ /**
3995
+ * The deploy entry of a library-origin asset from the library its lock entry records,
3996
+ * at the locked version when that library still holds it (else its newest). Other
3997
+ * libraries holding the same ref are not consulted: the lock already chose.
3998
+ *
3999
+ * @param want - Kind, name and optional publisher of the asset.
4000
+ * @returns `null` when the lock has no library-origin entry for it, or the recorded
4001
+ * library is gone or no longer holds it (callers fall back to a general lookup).
4002
+ */
4003
+ async lockedLibraryEntry(want) {
4004
+ const snap = await this.loadLibraryIndex();
4005
+ for (const [key, e] of Object.entries(this.tryReadLock()?.assets ?? {})) {
4006
+ const id = parseCanonicalLockKey(key);
4007
+ if (!e.library || e.library.installed || !id.publisher) continue;
4008
+ if (id.kind !== want.kind || id.name !== want.name) continue;
4009
+ if (want.publisher && id.publisher !== want.publisher) continue;
4010
+ const canonical = `${id.kind}:@${id.publisher}/${id.name}`;
4011
+ const refs = e.library.version ? [`${canonical}#${e.library.version}`, canonical] : [canonical];
4012
+ const lookup = this.lockedLibrary(e.library, snap);
4013
+ const library = lookup.state === "ok" ? lookup.name : e.library.name;
4014
+ for (const r of refs) {
4015
+ const hit = (await this.resolveLibraryEntries([r], { library })).entries[0];
4016
+ if (hit) return hit;
4017
+ }
4018
+ }
4019
+ return null;
4020
+ }
3217
4021
  /**
3218
4022
  * Produce a unified diff from the deployed asset to its catalog source: `-` lines
3219
4023
  * are in the deployed copy only (local edits `update` would overwrite), `+` lines
@@ -3231,10 +4035,16 @@ var AssetManager = class {
3231
4035
  async diff(ref) {
3232
4036
  const parsed = parseRefTolerant(ref);
3233
4037
  if (!parsed) return { status: "not-found" };
3234
- const entry = await this.info(ref);
4038
+ const entry = await this.lockedLibraryEntry(parsed) ?? await this.info(ref);
3235
4039
  if (!entry || !existsSync(entry.source)) return { status: "not-found" };
3236
4040
  const component = this.installTarget(parsed.kind);
4041
+ if (parsed.kind === "preset" && this.presetRecorded(parsed)) return { status: "no-files" };
3237
4042
  if (!component) return { status: "not-deployed" };
4043
+ if (component === "agent") {
4044
+ const diff = diffDeployedAgent(entry, this.deployOptsFor(this.driverTarget));
4045
+ if (diff === null) return { status: "not-deployed" };
4046
+ return diff === "" ? { status: "identical" } : { status: "differs", diff };
4047
+ }
3238
4048
  const deployedPath = join(
3239
4049
  deployedBase(component, this.driverTarget, this.global, this.projectDir),
3240
4050
  parsed.name
@@ -3413,7 +4223,10 @@ var AssetManager = class {
3413
4223
  continue;
3414
4224
  }
3415
4225
  const gaps = [];
3416
- for (const req of full.requires) {
4226
+ const requirements = dedupeRequirements(full.requires, publisher).map(
4227
+ (r) => r.publisher || !publisher ? r : { ...r, publisher }
4228
+ );
4229
+ for (const req of requirements) {
3417
4230
  const dep = memberIdentity(depToStr(req));
3418
4231
  if (!dep) continue;
3419
4232
  const depComponent = this.installTarget(dep.kind);
@@ -3432,7 +4245,10 @@ var AssetManager = class {
3432
4245
  if (!inRepos) gaps.push({ issue, ref: depToStr(req) });
3433
4246
  }
3434
4247
  }
3435
- const inLibrary = await this.resolveLibraryEntries(gaps.map((g) => g.ref));
4248
+ const inLibrary = await this.resolveLibraryEntries(
4249
+ gaps.map((g) => g.ref),
4250
+ { quiet: true }
4251
+ );
3436
4252
  for (const g of gaps) g.issue.inRepos = inLibrary.resolved.has(g.ref);
3437
4253
  }
3438
4254
  for (const [presetRef, gaps] of await this.declaredPresetGaps(dependencies, lock)) {
@@ -3453,30 +4269,52 @@ var AssetManager = class {
3453
4269
  return issues;
3454
4270
  }
3455
4271
  /**
3456
- * Everything the lock does not cover or contradicts, in one list: the set
3457
- * `skaile check`, `skaile install --locked` and `skaile outdated` all report.
4272
+ * The project's state as one verdict: every way the project differs from what it
4273
+ * declares and locks. `skaile check`, `skaile doctor`, `skaile outdated`, `skaile diff`
4274
+ * and `skaile install --locked` all take their verdict and exit code from this one list,
4275
+ * so one situation reads the same in all five.
3458
4276
  *
3459
4277
  * - `declared` — a `skaile.yaml` dependency with no lock entry (library or source gone,
3460
4278
  * unresolvable ref, never installed);
3461
4279
  * - `invalid` — a `dependencies:` entry that is not a parseable ref;
3462
- * - `drift` — declared + locked but its deployed files are missing;
4280
+ * - `drift` — a locked asset whose deployed files are missing;
3463
4281
  * - `pin` — a declared `#pin` that no lock entry for that asset satisfies;
3464
- * - `requires` — a deployed asset's `requires` that is not deployed (so not locked either);
3465
- * - `unresolved` — a lock-managed asset that no source or library holds any more.
4282
+ * - `requires` — a deployed asset's `requires` (or a declared preset's item) that is not
4283
+ * deployed (so not locked either);
4284
+ * - `unresolved` — a lock-managed deployed asset that no source or library holds any more;
4285
+ * - `library` — a locked library asset whose library is not registered (renamed or
4286
+ * removed) or no longer holds it, while another library holds it now;
4287
+ * - `incomplete` — a deployed library asset that lacks files of its library entry;
4288
+ * - `content` — a locked library asset whose files in the library changed since the lock
4289
+ * (or whose lock hash predates the one shared asset hash); `skaile install` re-locks it;
4290
+ * - `agent` — an `agents.<id>.ref` (or `agent.definition`) that binds to no agent.
4291
+ *
4292
+ * An asset appears once for what the lock says about it (`unresolved`, else `library`,
4293
+ * else `drift`). A declared ref no registered library or source holds names
4294
+ * `skaile library init` / `skaile remove`, never `skaile install`: `install` has nothing
4295
+ * to resolve it from.
3466
4296
  *
3467
- * Project mode only: a global manager (`global: true`) returns `[]`. A project without
3468
- * `skaile.yaml` has no declared set, so only the `requires` / `unresolved` gaps of
3469
- * deployed assets can appear.
4297
+ * Project mode only: a global manager (`global: true`) has no gaps. A project without
4298
+ * `skaile.yaml` has no declared set, so only the lock and deployed-asset gaps can appear.
3470
4299
  *
3471
- * @returns One {@link LockGap} per gap; empty when the lock covers everything declared.
4300
+ * @returns The {@link ProjectState}; `gaps` is empty when the project is as declared and locked.
3472
4301
  */
3473
- async lockGaps() {
3474
- if (this.global) return [];
3475
- const gaps = [];
3476
- const issues = await this.doctor();
3477
- for (const i of issues) {
4302
+ async projectState() {
4303
+ if (this.global) return { gaps: [], requirements: [] };
4304
+ const lock = this.tryReadLock();
4305
+ const lockedIds = Object.keys(lock?.assets ?? {}).map(parseCanonicalLockKey);
4306
+ const requirements = await this.doctor();
4307
+ let gaps = [];
4308
+ const lockIdentity = (kind, name) => {
4309
+ const publisher = lockedIds.find((id) => id.kind === kind && id.name === name)?.publisher;
4310
+ return publisher ? `${kind}:@${publisher}/${name}` : `${kind}:${name}`;
4311
+ };
4312
+ for (const i of requirements) {
3478
4313
  if (i.unresolved) {
3479
- const ref = `${i.assetKind}:${i.assetName}`;
4314
+ const publisher = lockedIds.find(
4315
+ (id) => id.kind === i.assetKind && id.name === i.assetName
4316
+ )?.publisher;
4317
+ const ref = publisher ? `${i.assetKind}:@${publisher}/${i.assetName}` : `${i.assetKind}:${i.assetName}`;
3480
4318
  const reason = `is locked but no source or library provides it any more \u2014 its requirements cannot be checked${i.fix ? ` (${i.fix})` : ""}`;
3481
4319
  gaps.push({
3482
4320
  type: "unresolved",
@@ -3487,27 +4325,351 @@ var AssetManager = class {
3487
4325
  });
3488
4326
  continue;
3489
4327
  }
3490
- const asset = `${i.assetKind}:${i.assetName}`;
4328
+ const asset = lockIdentity(i.assetKind, i.assetName);
3491
4329
  const dep = `${i.depKind}:${i.depName}`;
3492
- const fix = i.inRepos ? "fixable" : "not found";
4330
+ if (i.inRepos) {
4331
+ gaps.push({
4332
+ type: "requires",
4333
+ ref: dep,
4334
+ asset,
4335
+ reason: `is required by ${asset} but neither deployed nor locked (fixable)`,
4336
+ message: `${asset} requires ${dep} (fixable)`
4337
+ });
4338
+ } else {
4339
+ const fix = requirementNoHolderFix(asset);
4340
+ gaps.push({
4341
+ type: "requires",
4342
+ ref: dep,
4343
+ asset,
4344
+ reason: `is required by ${asset} but neither deployed nor locked (not found \u2014 ${fix})`,
4345
+ message: `${asset} requires ${dep} (not found \u2014 ${fix})`,
4346
+ manualFix: true
4347
+ });
4348
+ }
4349
+ }
4350
+ const named = new Set(
4351
+ gaps.filter((g) => g.type === "unresolved").map((g) => gapIdentity(g.ref))
4352
+ );
4353
+ for (const gap of await this.libraryOriginGaps(named)) {
4354
+ gaps.push(gap);
4355
+ named.add(gapIdentity(gap.ref));
4356
+ }
4357
+ const libraryHeld = (dep) => {
4358
+ const want = parseRefTolerant(dep);
4359
+ return want !== null && gaps.some((g) => g.type === "library" && sameAsset(g.ref, want));
4360
+ };
4361
+ gaps = gaps.filter((g) => !(g.type === "requires" && libraryHeld(g.ref)));
4362
+ for (const gap of this.undeployedLockGaps(lock, named)) {
4363
+ gaps.push(gap);
4364
+ named.add(gapIdentity(gap.ref));
4365
+ }
4366
+ for (const d of await this.lockedCopyDrift()) {
4367
+ if (named.has(gapIdentity(d.ref))) continue;
4368
+ const parts = [
4369
+ ...d.modified.map((f) => `${f} was edited`),
4370
+ ...d.extra.map((f) => `${f} is not part of the asset`)
4371
+ ];
4372
+ const reason = `is locked, but its deployed copy differs from the lock (${parts.join("; ")}) \u2014 run \`skaile install --locked\` to restore it`;
4373
+ gaps.push({ type: "modified", ref: d.ref, reason, message: `${d.ref} ${reason}` });
4374
+ named.add(gapIdentity(d.ref));
4375
+ }
4376
+ gaps.push(...await this.declaredGaps(requirements, named));
4377
+ for (const d of await this.incompleteDeploys()) {
4378
+ const reason = `is deployed without ${d.missing.join(", ")} \u2014 run \`skaile install\` to restore it`;
4379
+ gaps.push({ type: "incomplete", ref: d.ref, reason, message: `${d.ref} ${reason}` });
4380
+ }
4381
+ for (const u of unboundAgentInstances(this.projectDir, this.driverTarget)) {
4382
+ const reason = u.ref.startsWith("agent:") ? await this.unboundAgentReason(u.id, u.ref) : `is bound by agent instance "${u.id}" but resolves to no agent.yaml \u2014 fix the path, or drop the binding from skaile.yaml`;
3493
4383
  gaps.push({
3494
- type: "requires",
3495
- ref: dep,
3496
- reason: `is required by ${asset} but neither deployed nor locked (${fix})`,
3497
- message: `${asset} requires ${dep} (${fix})`
4384
+ type: "agent",
4385
+ ref: u.ref,
4386
+ reason,
4387
+ message: `${u.ref} ${reason}`,
4388
+ manualFix: true
3498
4389
  });
3499
4390
  }
3500
- let declared = [];
4391
+ return {
4392
+ gaps,
4393
+ requirements: requirements.filter(
4394
+ (i) => i.unresolved || !libraryHeld(`${i.depKind}:${i.depName}`)
4395
+ )
4396
+ };
4397
+ }
4398
+ /**
4399
+ * Why the `agent:` ref of agent instance `id` binds to nothing, as the phrase that follows
4400
+ * the ref in a {@link LockGap}; see {@link AssetManager.unboundAgentAdvice}.
4401
+ */
4402
+ async unboundAgentReason(id, ref) {
4403
+ const { lead, advice } = await this.unboundAgentAdvice(ref);
4404
+ return `is bound by agent instance "${id}" but ${lead} \u2014 ${advice}`;
4405
+ }
4406
+ /**
4407
+ * What is wrong with an `agent:` ref bound by an agent instance and what to do (`lead`, then
4408
+ * `advice`). `skaile add` is named only when a registered library or source holds the agent
4409
+ * and `add` takes the ref (it needs a publisher); otherwise the user registers the library
4410
+ * that holds it, writes the publisher, or drops the binding.
4411
+ */
4412
+ async unboundAgentAdvice(ref) {
4413
+ const drop = "or drop the binding from skaile.yaml";
4414
+ const publisher = parseRefTolerant(ref)?.publisher;
4415
+ if (publisher && await this.info(ref) !== null) {
4416
+ return {
4417
+ lead: "no agent was added for it",
4418
+ advice: `run \`skaile add ${ref}\` first, ${drop}`
4419
+ };
4420
+ }
4421
+ if (!publisher) {
4422
+ return {
4423
+ lead: "names no publisher",
4424
+ advice: `write it as agent:@<publisher>/<name> (\`skaile add\` needs the publisher), ${drop}`
4425
+ };
4426
+ }
4427
+ const fix = (await this.unresolvedFixes([ref])).get(ref);
4428
+ if (fix) {
4429
+ const register = fix.replace(/, or drop the dependency with `skaile remove [^`]*`$/, "");
4430
+ return {
4431
+ lead: "no agent was added for it, and `skaile add` cannot resolve it",
4432
+ advice: `${register}, ${drop}`
4433
+ };
4434
+ }
4435
+ return {
4436
+ lead: "no registered library or source holds it",
4437
+ advice: `register the library that holds it with \`skaile library init <name> --path <dir>\`, ${drop}`
4438
+ };
4439
+ }
4440
+ /** @deprecated Use `(await projectState()).gaps`; removed in the next major. */
4441
+ async lockGaps() {
4442
+ return (await this.projectState()).gaps;
4443
+ }
4444
+ /**
4445
+ * The {@link ProjectState} gaps that name `ref`: as the gap's own ref, or as the asset
4446
+ * whose requirement is unmet. A ref without a publisher matches any publisher.
4447
+ *
4448
+ * @param ref - Asset ref in `kind:name` or `kind:@publisher/name` form.
4449
+ */
4450
+ async gapsOf(ref) {
4451
+ const want = parseRefTolerant(ref);
4452
+ if (!want) return [];
4453
+ const names = (gapRef) => {
4454
+ const p = parseRefTolerant(gapRef);
4455
+ return p !== null && p.kind === want.kind && p.name === want.name && (!want.publisher || !p.publisher || p.publisher === want.publisher);
4456
+ };
4457
+ return (await this.projectState()).gaps.filter(
4458
+ (g) => names(g.ref) || g.asset && names(g.asset)
4459
+ );
4460
+ }
4461
+ /**
4462
+ * Locked library assets whose library cannot supply them: the library is not registered
4463
+ * (renamed or removed) or no longer holds the asset. Another library holding it makes the
4464
+ * gap fixable by `skaile install` (it re-locks); otherwise the user registers the library
4465
+ * again or drops the dependency.
4466
+ *
4467
+ * @param named - `kind:name` identities already reported; they are skipped.
4468
+ */
4469
+ async libraryOriginGaps(named) {
4470
+ const locked = Object.entries(this.tryReadLock()?.assets ?? {}).filter(
4471
+ ([, e]) => e.library !== void 0 && !e.library.installed
4472
+ );
4473
+ if (locked.length === 0) return [];
4474
+ const snap = await this.loadLibraryIndex();
4475
+ const scans = /* @__PURE__ */ new Map();
4476
+ const diskHashes = /* @__PURE__ */ new Map();
4477
+ const gaps = [];
4478
+ for (const [key, entry] of locked) {
4479
+ const origin = entry.library;
4480
+ const { publisher, kind, name } = parseCanonicalLockKey(key);
4481
+ if (!publisher || !kind || !name || named.has(`${kind}:${name}`)) continue;
4482
+ const ref = `${kind}:@${publisher}/${name}`;
4483
+ const lookup = this.lockedLibrary(origin, snap);
4484
+ const libPath = lookup.state === "ok" ? lookup.path : null;
4485
+ if (lookup.state === "ok" && libPath) {
4486
+ const scanKey = `${libPath}\0${publisher}`;
4487
+ let scanned = scans.get(scanKey);
4488
+ if (!scanned) {
4489
+ scanned = await scanDirectory(libPath, publisher, { knownKinds: this.knownKinds });
4490
+ scans.set(scanKey, scanned);
4491
+ }
4492
+ const found = scanned.find((x) => x.kind === kind && x.name === name);
4493
+ if (found) {
4494
+ const onDisk = origin.sha256 ? libraryDiskHashes(libPath, diskHashes, this.registry).get(`${kind}:${name}`) : void 0;
4495
+ if (onDisk !== void 0 && onDisk !== origin.sha256) {
4496
+ const diskVersion = found.version || newestLibraryDef(snap, lookup.name, { kind, publisher, name })?.version;
4497
+ if (!diskVersion || !gt(coerceVersion(diskVersion), coerceVersion(origin.version))) {
4498
+ const reason2 = `is locked, but library "${lookup.name}" content changed since lock \u2014 or the lock predates the one shared asset hash; run \`skaile install\` to re-lock it`;
4499
+ gaps.push({ type: "content", ref, reason: reason2, message: `${ref} ${reason2}` });
4500
+ }
4501
+ }
4502
+ continue;
4503
+ }
4504
+ }
4505
+ if (lookup.state === "dir-missing") {
4506
+ const reason2 = `is locked, but ${lockedLibraryProblem(lookup, origin, ref)}`;
4507
+ gaps.push({ type: "library", ref, reason: reason2, message: `${ref} ${reason2}`, manualFix: true });
4508
+ continue;
4509
+ }
4510
+ if (lookup.state === "unregistered") {
4511
+ const reason2 = `is locked, but library "${origin.name}" is not registered (renamed or removed?) \u2014 ${unregisteredLibraryFix(origin, ref)}`;
4512
+ gaps.push({ type: "library", ref, reason: reason2, message: `${ref} ${reason2}`, manualFix: true });
4513
+ continue;
4514
+ }
4515
+ const heldElsewhere = (await this.resolveLibraryEntries([ref], { quiet: true })).resolved.size > 0;
4516
+ if (heldElsewhere) {
4517
+ const reason2 = `is locked, but library "${lookup.name}" no longer holds it \u2014 run \`skaile install\` to re-lock it against the library that holds it now`;
4518
+ gaps.push({ type: "library", ref, reason: reason2, message: `${ref} ${reason2}` });
4519
+ continue;
4520
+ }
4521
+ const reason = `is locked, but library "${lookup.name}" no longer holds it \u2014 restore it there, or drop the dependency with \`skaile remove ${ref}\``;
4522
+ gaps.push({ type: "library", ref, reason, message: `${ref} ${reason}`, manualFix: true });
4523
+ }
4524
+ return gaps;
4525
+ }
4526
+ /**
4527
+ * The {@link RestoredAsset} rows for the drifted copies (see {@link lockedCopyDrift}) a
4528
+ * deploy just wrote again: those whose ref is among the `deployed` canonical refs.
4529
+ */
4530
+ restoredCopies(drifts, deployed) {
4531
+ const rewritten = new Set(deployed.map((d) => d.split("#")[0]));
4532
+ const out = [];
4533
+ for (const d of drifts) {
4534
+ if (!rewritten.has(d.ref)) continue;
4535
+ const { kind, name } = parseCanonicalLockKey(d.key);
4536
+ const component = kind ? this.installTarget(kind) : void 0;
4537
+ if (!kind || !name || !component) continue;
4538
+ const dir = relative(
4539
+ this.projectDir,
4540
+ deployedDir(component, name, this.driverTarget, this.global, this.projectDir)
4541
+ ).split(sep).join("/");
4542
+ out.push({
4543
+ ref: d.key,
4544
+ restored: d.modified.map((f) => `${dir}/${f}`),
4545
+ removed: d.extra.map((f) => `${dir}/${f}`)
4546
+ });
4547
+ }
4548
+ return out;
4549
+ }
4550
+ /**
4551
+ * Locked assets whose deployed copy differs from the bytes the lock covers: a file edited by
4552
+ * hand, or a file the asset does not ship. One row per asset, `ref` being `kind:@publisher/name`
4553
+ * and `key` its lock key. The one comparison behind the `modified` gap and the `Restored` lines
4554
+ * of `install`.
4555
+ *
4556
+ * - A library-origin asset is compared with its library's files, but only while those equal the
4557
+ * locked hash (changed library content is the `content` gap's business), both modified and
4558
+ * extra files.
4559
+ * - A source asset is compared with the per-file hashes the lock records (modified files only:
4560
+ * the lock lists what the source covers, not what the deploy copies).
4561
+ *
4562
+ * An agent (rendered, not copied), a `--link` deploy, an asset with a configured patch, and a
4563
+ * copy that is missing are not compared: other gaps cover them. Project mode only.
4564
+ */
4565
+ async lockedCopyDrift() {
4566
+ if (this.global) return [];
4567
+ const lock = this.tryReadLock();
4568
+ if (!lock) return [];
4569
+ const { patches } = this.loadConfig();
4570
+ const out = [];
4571
+ const deployOpts = {
4572
+ registry: this.registry,
4573
+ driverTarget: this.driverTarget,
4574
+ global: this.global,
4575
+ cwd: this.projectDir
4576
+ };
4577
+ const refOf = (key) => {
4578
+ const { kind, publisher, name } = parseCanonicalLockKey(key);
4579
+ return publisher ? `${kind}:@${publisher}/${name}` : `${kind}:${name}`;
4580
+ };
4581
+ const keyOf = /* @__PURE__ */ new Map();
4582
+ for (const key of Object.keys(lock.assets)) keyOf.set(refOf(key), key);
4583
+ const { entries } = await this.restoreLibraryLockEntries(lock);
4584
+ for (const e of entries) {
4585
+ if (patches[`${e.kind}:${e.name}`]) continue;
4586
+ const drift = deployedAssetDrift(e, deployOpts);
4587
+ if (!drift || drift.modified.length === 0 && drift.extra.length === 0) continue;
4588
+ const ref = `${e.kind}:@${(e.publisher ?? "").replace(/^@/, "")}/${e.name}`;
4589
+ out.push({ ref, key: keyOf.get(ref) ?? ref, ...drift, library: true });
4590
+ }
4591
+ for (const [key, entry] of Object.entries(lock.assets)) {
4592
+ if (!entry.source || entry.files.length === 0) continue;
4593
+ const { kind, name } = parseCanonicalLockKey(key);
4594
+ const component = kind ? this.installTarget(kind) : void 0;
4595
+ if (!kind || !name || !component || component === "agent") continue;
4596
+ if (patches[`${kind}:${name}`]) continue;
4597
+ const dest = deployedDir(component, name, this.driverTarget, this.global, this.projectDir);
4598
+ try {
4599
+ const deployed = lstatSync(dest);
4600
+ if (deployed.isSymbolicLink() || !deployed.isDirectory()) continue;
4601
+ } catch {
4602
+ continue;
4603
+ }
4604
+ const dirs = entry.files.map(
4605
+ (f) => posix.dirname(f.path).split("/").filter((s) => s !== ".")
4606
+ );
4607
+ let common = dirs[0] ?? [];
4608
+ for (const d of dirs.slice(1)) {
4609
+ let i = 0;
4610
+ while (i < common.length && i < d.length && common[i] === d[i]) i++;
4611
+ common = common.slice(0, i);
4612
+ }
4613
+ const prefix = common.length > 0 ? `${common.join("/")}/` : "";
4614
+ const modified = [];
4615
+ for (const f of entry.files) {
4616
+ const rel = f.path.startsWith(prefix) ? f.path.slice(prefix.length) : f.path;
4617
+ const deployedFile = join(dest, rel);
4618
+ if (!existsSync(deployedFile)) continue;
4619
+ try {
4620
+ if (computeHash(deployedFile) !== f.sha256) modified.push(rel);
4621
+ } catch {
4622
+ modified.push(rel);
4623
+ }
4624
+ }
4625
+ if (modified.length > 0)
4626
+ out.push({ ref: refOf(key), key, modified, extra: [], library: false });
4627
+ }
4628
+ return out;
4629
+ }
4630
+ /**
4631
+ * Locked assets (declared or required by one) whose deployed copy is gone: `skaile install`
4632
+ * restores them. Same deployability rule as {@link AssetManager.listDeclared}'s `drift`.
4633
+ *
4634
+ * @param named - `kind:name` identities already reported; they are skipped.
4635
+ */
4636
+ undeployedLockGaps(lock, named) {
4637
+ const gaps = [];
4638
+ for (const [key, entry] of Object.entries(lock?.assets ?? {})) {
4639
+ if (entry.library?.installed) continue;
4640
+ const { publisher, kind, name } = parseCanonicalLockKey(key);
4641
+ const component = kind ? this.installTarget(kind) : void 0;
4642
+ if (!kind || !name || !component || named.has(`${kind}:${name}`)) continue;
4643
+ if (isDeployed(component, name, this.driverTarget, this.global, this.projectDir)) continue;
4644
+ const ref = publisher ? `${kind}:@${publisher}/${name}` : `${kind}:${name}`;
4645
+ const reason = "is locked but its deployed copy is missing \u2014 run `skaile install`";
4646
+ gaps.push({ type: "drift", ref, reason, message: `${ref} ${reason}` });
4647
+ }
4648
+ return gaps;
4649
+ }
4650
+ /**
4651
+ * The `skaile.yaml` side of {@link AssetManager.projectState}: declared refs that do not
4652
+ * parse, never resolved into the lock, are not materialized, or contradict a `#pin`.
4653
+ * A project with no (or an unreadable) `skaile.yaml` has no declared set to reconcile.
4654
+ *
4655
+ * @param requirements - What {@link AssetManager.doctor} found; a declared preset whose
4656
+ * items it already named needs no second line.
4657
+ * @param named - `kind:name` identities already reported; a declared asset among them
4658
+ * needs no `drift` line.
4659
+ */
4660
+ async declaredGaps(requirements, named) {
4661
+ let declared;
3501
4662
  try {
3502
4663
  declared = await this.listDeclared();
3503
4664
  } catch {
3504
- return gaps;
4665
+ return [];
3505
4666
  }
4667
+ const gaps = [];
3506
4668
  const unlocked = declared.filter((d) => d.state === "declared").map((d) => d.ref);
3507
4669
  const hints = await this.publisherChangeHints(unlocked);
3508
4670
  const lockKeys = Object.keys(this.tryReadLock()?.assets ?? {});
3509
4671
  for (const d of declared) {
3510
- if (d.kind === "preset" && issues.some((i) => i.assetKind === "preset" && i.assetName === d.name)) {
4672
+ if (d.kind === "preset" && requirements.some((i) => i.assetKind === "preset" && i.assetName === d.name)) {
3511
4673
  continue;
3512
4674
  }
3513
4675
  let gap;
@@ -3517,19 +4679,41 @@ var AssetManager = class {
3517
4679
  reason: "is not a valid asset ref (expected kind:@<publisher>/<name>#<version>)"
3518
4680
  };
3519
4681
  } else if (d.state === "drift") {
4682
+ if (named.has(`${d.kind}:${d.name}`)) continue;
3520
4683
  gap = {
3521
4684
  type: "drift",
3522
4685
  reason: "is declared and locked but not materialized \u2014 run `skaile install`"
3523
4686
  };
3524
4687
  } else if (d.state === "declared") {
3525
4688
  const hint = hints[d.ref];
3526
- gap = hint ? { type: "declared", reason: `is declared but does not resolve${hint}`, manualFix: true } : {
3527
- type: "declared",
3528
- reason: "is declared but does not resolve \u2014 run `skaile install`"
3529
- };
4689
+ if (hint) {
4690
+ gap = {
4691
+ type: "declared",
4692
+ reason: `is declared but does not resolve${hint}`,
4693
+ manualFix: true
4694
+ };
4695
+ } else if (await this.declaredRefResolvable(d.ref)) {
4696
+ gap = { type: "declared", reason: "is declared but not locked \u2014 run `skaile install`" };
4697
+ } else {
4698
+ gap = {
4699
+ type: "declared",
4700
+ reason: declaredNoHolderReason(d.ref),
4701
+ manualFix: true
4702
+ };
4703
+ }
3530
4704
  } else {
3531
4705
  const want = parseAssetRef(d.ref);
3532
- if (!want.pin || want.kind === "preset") continue;
4706
+ if (want.kind === "preset") {
4707
+ if (this.presetRecorded(want) || await this.info(d.ref) !== null) continue;
4708
+ gap = await this.declaredRefResolvable(d.ref) ? { type: "declared", reason: "is declared but not locked \u2014 run `skaile install`" } : {
4709
+ type: "declared",
4710
+ reason: declaredNoHolderReason(d.ref),
4711
+ manualFix: true
4712
+ };
4713
+ gaps.push({ ...gap, ref: d.ref, message: `${d.ref} ${gap.reason}` });
4714
+ continue;
4715
+ }
4716
+ if (!want.pin) continue;
3533
4717
  const held = lockKeys.map((key) => ({ key, id: parseCanonicalLockKey(key) })).filter(
3534
4718
  ({ id }) => id.kind === want.kind && id.name === want.name && (!want.publisher || id.publisher === want.publisher)
3535
4719
  );
@@ -3544,6 +4728,22 @@ var AssetManager = class {
3544
4728
  }
3545
4729
  return gaps;
3546
4730
  }
4731
+ /** Whether the lock's `presets` slice records `want` (kind, name and, when both carry one, publisher). */
4732
+ presetRecorded(want) {
4733
+ return (this.tryReadLock()?.presets ?? []).some((ref) => {
4734
+ const p = parseRefTolerant(ref);
4735
+ return p !== null && p.kind === want.kind && p.name === want.name && (!want.publisher || !p.publisher || p.publisher === want.publisher);
4736
+ });
4737
+ }
4738
+ /**
4739
+ * Whether `skaile install` can resolve a declared ref: a source or library holds it, or the
4740
+ * project declares a store that may. The global catalog is not asked: a check never goes
4741
+ * to the network.
4742
+ */
4743
+ async declaredRefResolvable(ref) {
4744
+ if (await this.info(ref) !== null) return true;
4745
+ return this.loadConfig().stores.length > 0;
4746
+ }
3547
4747
  /**
3548
4748
  * Items of each declared `preset:` dependency that the lock does not cover.
3549
4749
  *
@@ -3810,6 +5010,54 @@ var AssetManager = class {
3810
5010
  writeFileSync(configPath, stringify(raw, { lineWidth: 120 }));
3811
5011
  return true;
3812
5012
  }
5013
+ /**
5014
+ * Drop the agent instances of `skaile.yaml` (`agents.<id>`, or the `agent.definition`
5015
+ * default) whose `ref` binds to `target`, and the native agent files an install rendered for
5016
+ * them. Only an `agent` target has bindings; project mode only. A `ref` without a publisher
5017
+ * matches any publisher, and a pin is ignored.
5018
+ *
5019
+ * @returns `true` when a binding was removed.
5020
+ */
5021
+ removeAgentBindings(target) {
5022
+ if (this.global || target.kind !== "agent") return false;
5023
+ const configPath = join(this.stateRoot, "skaile.yaml");
5024
+ if (!existsSync(configPath)) return false;
5025
+ let raw;
5026
+ try {
5027
+ raw = parse(readFileSync(configPath, "utf8"));
5028
+ } catch {
5029
+ return false;
5030
+ }
5031
+ if (!raw || typeof raw !== "object") return false;
5032
+ const wantPub = target.publisher?.replace(/^@/, "");
5033
+ const binds = (ref) => {
5034
+ const p = typeof ref === "string" ? parseRefTolerant(ref) : null;
5035
+ if (!p || p.kind !== "agent" || p.name !== target.name) return false;
5036
+ const havePub = p.publisher?.replace(/^@/, "");
5037
+ return wantPub === void 0 || havePub === void 0 || havePub === wantPub;
5038
+ };
5039
+ const ids = [];
5040
+ const instances = raw.agents;
5041
+ if (instances && typeof instances === "object") {
5042
+ for (const [id, instance] of Object.entries(instances)) {
5043
+ if (!binds(instance?.ref)) continue;
5044
+ ids.push(id);
5045
+ delete instances[id];
5046
+ }
5047
+ if (Object.keys(instances).length === 0) delete raw.agents;
5048
+ }
5049
+ if (raw.agent && typeof raw.agent === "object" && binds(raw.agent.definition)) {
5050
+ ids.push(DEFAULT_AGENT_INSTANCE_ID);
5051
+ delete raw.agent.definition;
5052
+ if (Object.keys(raw.agent).length === 0) delete raw.agent;
5053
+ }
5054
+ if (ids.length === 0) return false;
5055
+ writeFileSync(configPath, stringify(raw, { lineWidth: 120 }));
5056
+ for (const driverTarget of this.targetHarnesses()) {
5057
+ removeAgentInstanceRenderings({ projectDir: this.projectDir, driverTarget, ids });
5058
+ }
5059
+ return true;
5060
+ }
3813
5061
  resolveRepoDir(decl, name) {
3814
5062
  const links = readLinks(this.projectDir);
3815
5063
  if (links[name]) {
@@ -3828,5 +5076,5 @@ var AssetManager = class {
3828
5076
  };
3829
5077
 
3830
5078
  export { AssetManager, FACTORY_SOURCE_URL, MissingManifestError, installAgentInstances, normalizeRepoUrl, parseCanonicalLockKey, parseRefTolerant, renderMcpServerConfig, softDepName };
3831
- //# sourceMappingURL=chunk-IMKSM5NL.js.map
3832
- //# sourceMappingURL=chunk-IMKSM5NL.js.map
5079
+ //# sourceMappingURL=chunk-DZMNFCWB.js.map
5080
+ //# sourceMappingURL=chunk-DZMNFCWB.js.map