@skaile/workspaces 0.29.1 → 0.31.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 (475) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/dist/{asset-feeds-2V7LZIG7.js → asset-feeds-6HZDZEIT.js} +14 -13
  3. package/dist/asset-feeds-6HZDZEIT.js.map +1 -0
  4. package/dist/asset-manager/index.js +12 -12
  5. package/dist/asset-manager/installer.js +11 -11
  6. package/dist/asset-manager/renderers.js +2 -2
  7. package/dist/asset-manager/src/index.d.ts +27 -0
  8. package/dist/asset-manager/src/index.d.ts.map +1 -1
  9. package/dist/asset-manager/src/install-agent.d.ts.map +1 -1
  10. package/dist/asset-manager/src/installer.d.ts +1 -2
  11. package/dist/asset-manager/src/installer.d.ts.map +1 -1
  12. package/dist/asset-manager/src/renderers.d.ts.map +1 -1
  13. package/dist/bridge/drivers/claude-sdk.js +354 -224
  14. package/dist/bridge/drivers/claude-sdk.js.map +1 -1
  15. package/dist/bridge/drivers/codex.js +3 -3
  16. package/dist/bridge/drivers/codex.js.map +1 -1
  17. package/dist/bridge/drivers/continue.js +4 -4
  18. package/dist/bridge/drivers/echo.js +2 -2
  19. package/dist/bridge/drivers/echo.js.map +1 -1
  20. package/dist/bridge/drivers/gemini.js +5 -5
  21. package/dist/bridge/drivers/goose.js +4 -4
  22. package/dist/bridge/drivers/omp.js +3 -3
  23. package/dist/bridge/drivers/opencode.js +4 -4
  24. package/dist/bridge/drivers/qwen.js +5 -5
  25. package/dist/bridge/index.js +4 -4
  26. package/dist/bridge/src/drivers/claude-sdk.d.ts +68 -0
  27. package/dist/bridge/src/drivers/claude-sdk.d.ts.map +1 -1
  28. package/dist/bridge/src/drivers/echo.d.ts +1 -1
  29. package/dist/bridge/src/drivers/echo.d.ts.map +1 -1
  30. package/dist/bridge/src/error-classifier.d.ts.map +1 -1
  31. package/dist/bridge/src/normalizer.d.ts +5 -0
  32. package/dist/bridge/src/normalizer.d.ts.map +1 -1
  33. package/dist/{chunk-LDUNTZB6.js → chunk-26L2B6P6.js} +2 -2
  34. package/dist/{chunk-LDUNTZB6.js.map → chunk-26L2B6P6.js.map} +1 -1
  35. package/dist/{chunk-DVMSGGCM.js → chunk-45RKEHPX.js} +158 -108
  36. package/dist/chunk-45RKEHPX.js.map +1 -0
  37. package/dist/{chunk-2FS4FPZ7.js → chunk-4AGIEU7V.js} +44 -62
  38. package/dist/chunk-4AGIEU7V.js.map +1 -0
  39. package/dist/{chunk-SF4GWVR5.js → chunk-4CRKROS5.js} +86 -55
  40. package/dist/chunk-4CRKROS5.js.map +1 -0
  41. package/dist/{chunk-ZONCCO5N.js → chunk-4R6DROGN.js} +30 -32
  42. package/dist/chunk-4R6DROGN.js.map +1 -0
  43. package/dist/{chunk-MBOR7D65.js → chunk-67MFR5LS.js} +3 -3
  44. package/dist/{chunk-MBOR7D65.js.map → chunk-67MFR5LS.js.map} +1 -1
  45. package/dist/{chunk-7GWYRVAX.js → chunk-6LW7Q53Z.js} +162 -165
  46. package/dist/chunk-6LW7Q53Z.js.map +1 -0
  47. package/dist/{chunk-QMONOHXT.js → chunk-74UKG2LA.js} +3 -3
  48. package/dist/{chunk-QMONOHXT.js.map → chunk-74UKG2LA.js.map} +1 -1
  49. package/dist/{chunk-AKSR434V.js → chunk-7FLLPKX3.js} +79 -56
  50. package/dist/chunk-7FLLPKX3.js.map +1 -0
  51. package/dist/{chunk-BZ2JU7IP.js → chunk-7KOBITAP.js} +5 -5
  52. package/dist/{chunk-BZ2JU7IP.js.map → chunk-7KOBITAP.js.map} +1 -1
  53. package/dist/{chunk-JIQLLZ65.js → chunk-7MANNJ3U.js} +534 -235
  54. package/dist/chunk-7MANNJ3U.js.map +1 -0
  55. package/dist/{chunk-7R6W5EQR.js → chunk-ACXLKNIM.js} +2 -2
  56. package/dist/{chunk-7R6W5EQR.js.map → chunk-ACXLKNIM.js.map} +1 -1
  57. package/dist/{chunk-UIZKIUC6.js → chunk-BRBOC53X.js} +3 -3
  58. package/dist/chunk-BRBOC53X.js.map +1 -0
  59. package/dist/{chunk-FWHOU3CX.js → chunk-CBIPUBUS.js} +6 -6
  60. package/dist/{chunk-FWHOU3CX.js.map → chunk-CBIPUBUS.js.map} +1 -1
  61. package/dist/{chunk-3QQSSRYN.js → chunk-DKAV43NI.js} +430 -346
  62. package/dist/chunk-DKAV43NI.js.map +1 -0
  63. package/dist/{chunk-M4KLIEIJ.js → chunk-G247J4MC.js} +3 -3
  64. package/dist/chunk-G247J4MC.js.map +1 -0
  65. package/dist/{chunk-TV72ZJYN.js → chunk-GXC3TYMQ.js} +60 -47
  66. package/dist/chunk-GXC3TYMQ.js.map +1 -0
  67. package/dist/{chunk-IERB3UXJ.js → chunk-H5YXL4KI.js} +83 -69
  68. package/dist/chunk-H5YXL4KI.js.map +1 -0
  69. package/dist/{chunk-2NOE4AGD.js → chunk-HH62ECJ7.js} +3 -3
  70. package/dist/{chunk-2NOE4AGD.js.map → chunk-HH62ECJ7.js.map} +1 -1
  71. package/dist/{chunk-QDJKE557.js → chunk-HRKB2ATT.js} +64 -64
  72. package/dist/chunk-HRKB2ATT.js.map +1 -0
  73. package/dist/{chunk-MLJKA3S6.js → chunk-IENYMRZ6.js} +1711 -1497
  74. package/dist/chunk-IENYMRZ6.js.map +1 -0
  75. package/dist/{chunk-24UIWON4.js → chunk-JB35MVBN.js} +167 -156
  76. package/dist/chunk-JB35MVBN.js.map +1 -0
  77. package/dist/{chunk-5YO3H5Q4.js → chunk-KY5NY7LB.js} +3 -3
  78. package/dist/chunk-KY5NY7LB.js.map +1 -0
  79. package/dist/{chunk-MIH45XPY.js → chunk-LA2YSXEE.js} +1974 -1919
  80. package/dist/chunk-LA2YSXEE.js.map +1 -0
  81. package/dist/{chunk-IEJ5IOC3.js → chunk-LFDZYCEO.js} +4 -2
  82. package/dist/chunk-LFDZYCEO.js.map +1 -0
  83. package/dist/{chunk-72IA6BTP.js → chunk-MWAEOCU7.js} +9 -9
  84. package/dist/{chunk-72IA6BTP.js.map → chunk-MWAEOCU7.js.map} +1 -1
  85. package/dist/{chunk-5V5KZQIC.js → chunk-NGE6PALH.js} +4 -4
  86. package/dist/{chunk-5V5KZQIC.js.map → chunk-NGE6PALH.js.map} +1 -1
  87. package/dist/{chunk-PAYAFNXG.js → chunk-NGJ6E344.js} +104 -74
  88. package/dist/chunk-NGJ6E344.js.map +1 -0
  89. package/dist/{chunk-UBCZVONU.js → chunk-NQC7ZUXE.js} +3 -3
  90. package/dist/{chunk-UBCZVONU.js.map → chunk-NQC7ZUXE.js.map} +1 -1
  91. package/dist/{chunk-ZEKWHJBB.js → chunk-OEXA7BBK.js} +3 -3
  92. package/dist/{chunk-ZEKWHJBB.js.map → chunk-OEXA7BBK.js.map} +1 -1
  93. package/dist/{chunk-5RBPPBEK.js → chunk-P72K36PU.js} +4 -4
  94. package/dist/{chunk-5RBPPBEK.js.map → chunk-P72K36PU.js.map} +1 -1
  95. package/dist/{chunk-OTFG5Q7H.js → chunk-QF6LBS2C.js} +4594 -4480
  96. package/dist/chunk-QF6LBS2C.js.map +1 -0
  97. package/dist/{chunk-HK5FRWG6.js → chunk-R6O6YWU7.js} +2 -2
  98. package/dist/{chunk-HK5FRWG6.js.map → chunk-R6O6YWU7.js.map} +1 -1
  99. package/dist/{chunk-PRLD7EXG.js → chunk-SBGHNXJD.js} +2 -2
  100. package/dist/{chunk-PRLD7EXG.js.map → chunk-SBGHNXJD.js.map} +1 -1
  101. package/dist/{chunk-CMG3GJ6B.js → chunk-VO7QQ76S.js} +4 -4
  102. package/dist/{chunk-CMG3GJ6B.js.map → chunk-VO7QQ76S.js.map} +1 -1
  103. package/dist/{chunk-Z3XXKR5E.js → chunk-WDMH4UMD.js} +4 -6
  104. package/dist/chunk-WDMH4UMD.js.map +1 -0
  105. package/dist/{chunk-W7W66MDZ.js → chunk-X46FY45B.js} +44 -42
  106. package/dist/chunk-X46FY45B.js.map +1 -0
  107. package/dist/{chunk-JKNWJ64A.js → chunk-XGCXFX7X.js} +2 -2
  108. package/dist/{chunk-JKNWJ64A.js.map → chunk-XGCXFX7X.js.map} +1 -1
  109. package/dist/{chunk-FQHHCICT.js → chunk-XVDXGQ55.js} +226 -226
  110. package/dist/chunk-XVDXGQ55.js.map +1 -0
  111. package/dist/{chunk-OKRUTSG7.js → chunk-ZDKJHQY2.js} +2 -2
  112. package/dist/{chunk-OKRUTSG7.js.map → chunk-ZDKJHQY2.js.map} +1 -1
  113. package/dist/{chunk-LD4QIVBY.js → chunk-ZM7UOJR2.js} +3 -3
  114. package/dist/{chunk-LD4QIVBY.js.map → chunk-ZM7UOJR2.js.map} +1 -1
  115. package/dist/cli/index.js +2811 -2662
  116. package/dist/cli/index.js.map +1 -1
  117. package/dist/cli/src/asset-feeds.d.ts +0 -9
  118. package/dist/cli/src/asset-feeds.d.ts.map +1 -1
  119. package/dist/cli/src/commands/catalog-cmd.d.ts.map +1 -1
  120. package/dist/cli/src/commands/catalog.d.ts.map +1 -1
  121. package/dist/cli/src/commands/config.d.ts.map +1 -1
  122. package/dist/cli/src/commands/deploy.d.ts.map +1 -1
  123. package/dist/cli/src/commands/integration.d.ts.map +1 -1
  124. package/dist/cli/src/commands/library-cmd.d.ts.map +1 -1
  125. package/dist/cli/src/commands/manage.d.ts.map +1 -1
  126. package/dist/cli/src/commands/preset-cmd.d.ts.map +1 -1
  127. package/dist/cli/src/commands/project.d.ts +1 -1
  128. package/dist/cli/src/commands/project.d.ts.map +1 -1
  129. package/dist/cli/src/commands/show.d.ts.map +1 -1
  130. package/dist/cli/src/commands/source.d.ts.map +1 -1
  131. package/dist/cli/src/commands/validate.d.ts.map +1 -1
  132. package/dist/cli/src/ensure-sources.d.ts.map +1 -1
  133. package/dist/cli/src/flow-discovery.d.ts.map +1 -1
  134. package/dist/cli/src/open-registry.d.ts.map +1 -1
  135. package/dist/cli/src/plugin-store/index.d.ts +4 -6
  136. package/dist/cli/src/plugin-store/index.d.ts.map +1 -1
  137. package/dist/cli/src/plugin-store/paths.d.ts +0 -2
  138. package/dist/cli/src/plugin-store/paths.d.ts.map +1 -1
  139. package/dist/cli/src/setup.d.ts.map +1 -1
  140. package/dist/connectors/config.js +9 -9
  141. package/dist/connectors/index.js +12 -12
  142. package/dist/connectors/rclone.js +2 -2
  143. package/dist/connectors/src/connector-prompt.d.ts.map +1 -1
  144. package/dist/connectors/src/connector-registry.d.ts.map +1 -1
  145. package/dist/connectors/src/connector-types.d.ts +1 -1
  146. package/dist/connectors/src/connector-types.d.ts.map +1 -1
  147. package/dist/connectors/src/index.d.ts +5 -5
  148. package/dist/connectors/src/index.d.ts.map +1 -1
  149. package/dist/connectors/src/managed-gitconfig.d.ts.map +1 -1
  150. package/dist/{connectors-PTCSHCHZ.js → connectors-DY7UBBV5.js} +3 -3
  151. package/dist/{connectors-PTCSHCHZ.js.map → connectors-DY7UBBV5.js.map} +1 -1
  152. package/dist/connectors-shared/src/index.d.ts +1 -1
  153. package/dist/connectors-shared/src/index.d.ts.map +1 -1
  154. package/dist/core/index.js +8 -8
  155. package/dist/core/logging.js +1 -1
  156. package/dist/core/manifest.js +2 -2
  157. package/dist/core/models.js +1 -1
  158. package/dist/core/publish-manifest.js +2 -2
  159. package/dist/core/runtime-assets.js +5 -5
  160. package/dist/core/src/lock.d.ts +0 -9
  161. package/dist/core/src/lock.d.ts.map +1 -1
  162. package/dist/core/src/logging/create-logger.d.ts.map +1 -1
  163. package/dist/core/src/logging/index.d.ts +11 -11
  164. package/dist/core/src/logging/index.d.ts.map +1 -1
  165. package/dist/core/src/logging/log-store.d.ts.map +1 -1
  166. package/dist/core/src/logging/sinks/no-op-sink.d.ts.map +1 -1
  167. package/dist/core/src/logging/sinks/sqlite-sink.d.ts.map +1 -1
  168. package/dist/core/src/manifest.d.ts.map +1 -1
  169. package/dist/core/src/models.d.ts +6 -0
  170. package/dist/core/src/models.d.ts.map +1 -1
  171. package/dist/core/src/publish-manifest.d.ts.map +1 -1
  172. package/dist/core/src/repo-manager.d.ts +0 -12
  173. package/dist/core/src/repo-manager.d.ts.map +1 -1
  174. package/dist/core/src/runtime-assets.d.ts.map +1 -1
  175. package/dist/core/src/store.d.ts +1 -1
  176. package/dist/core/src/store.d.ts.map +1 -1
  177. package/dist/core/src/walker.d.ts.map +1 -1
  178. package/dist/core/src/workspace-config.d.ts +8 -27
  179. package/dist/core/src/workspace-config.d.ts.map +1 -1
  180. package/dist/core/store.js +1 -1
  181. package/dist/core/workspace-config.js +4 -4
  182. package/dist/deploy/index.js +8 -8
  183. package/dist/discovery/index.js +6 -6
  184. package/dist/discovery/src/asset-kind-registry.d.ts +1 -1
  185. package/dist/discovery/src/asset-kind-registry.d.ts.map +1 -1
  186. package/dist/discovery/src/builtin-providers.d.ts.map +1 -1
  187. package/dist/discovery/src/discover-manifest.d.ts +1 -1
  188. package/dist/discovery/src/discover-manifest.d.ts.map +1 -1
  189. package/dist/discovery/src/discover.d.ts +30 -3
  190. package/dist/discovery/src/discover.d.ts.map +1 -1
  191. package/dist/discovery/src/domains.d.ts +144 -0
  192. package/dist/discovery/src/domains.d.ts.map +1 -0
  193. package/dist/discovery/src/index.d.ts +9 -7
  194. package/dist/discovery/src/index.d.ts.map +1 -1
  195. package/dist/discovery/src/source-config.d.ts +1 -1
  196. package/dist/discovery/src/source-config.d.ts.map +1 -1
  197. package/dist/discovery/src/tree-entries.d.ts +13 -1
  198. package/dist/discovery/src/tree-entries.d.ts.map +1 -1
  199. package/dist/ensure-sources-JWBUVDW6.js +84 -0
  200. package/dist/ensure-sources-JWBUVDW6.js.map +1 -0
  201. package/dist/factory-assets/connectors/deploy/adapter.d.ts +1 -1
  202. package/dist/factory-assets/connectors/deploy/adapter.d.ts.map +1 -1
  203. package/dist/factory-assets/connectors/deploy.js +12 -12
  204. package/dist/factory-assets/connectors/devserver/adapter.d.ts +1 -1
  205. package/dist/factory-assets/connectors/devserver/adapter.d.ts.map +1 -1
  206. package/dist/factory-assets/connectors/devserver.js +12 -12
  207. package/dist/factory-assets/connectors/flow/adapter.d.ts +16 -2
  208. package/dist/factory-assets/connectors/flow/adapter.d.ts.map +1 -1
  209. package/dist/factory-assets/connectors/flow/adapter.js +12 -12
  210. package/dist/factory-assets/connectors/flow/engine/flow-kind-provider.d.ts.map +1 -1
  211. package/dist/factory-assets/connectors/flow/engine/index.d.ts +3 -3
  212. package/dist/factory-assets/connectors/flow/engine/index.d.ts.map +1 -1
  213. package/dist/factory-assets/connectors/flow/engine.js +9 -9
  214. package/dist/factory-assets/connectors/flow/index.d.ts +1 -1
  215. package/dist/factory-assets/connectors/flow/index.d.ts.map +1 -1
  216. package/dist/factory-assets/connectors/flow/run-flow.js +15 -15
  217. package/dist/factory-assets/connectors/flow.js +12 -12
  218. package/dist/factory-assets/connectors/git/driver.d.ts +32 -0
  219. package/dist/factory-assets/connectors/git/driver.d.ts.map +1 -1
  220. package/dist/factory-assets/connectors/git.js +12 -12
  221. package/dist/factory-assets/connectors/gmail/adapter.d.ts +1 -1
  222. package/dist/factory-assets/connectors/gmail/adapter.d.ts.map +1 -1
  223. package/dist/factory-assets/connectors/gmail.js +12 -12
  224. package/dist/factory-assets/connectors/googledrive.js +12 -12
  225. package/dist/factory-assets/connectors/local/driver.d.ts +1 -2
  226. package/dist/factory-assets/connectors/local/driver.d.ts.map +1 -1
  227. package/dist/factory-assets/connectors/local.js +12 -12
  228. package/dist/factory-assets/connectors/mattermost/adapter.d.ts +1 -1
  229. package/dist/factory-assets/connectors/mattermost/adapter.d.ts.map +1 -1
  230. package/dist/factory-assets/connectors/mattermost.js +12 -12
  231. package/dist/factory-assets/connectors/memory/adapter.d.ts.map +1 -1
  232. package/dist/factory-assets/connectors/memory.js +12 -12
  233. package/dist/factory-assets/connectors/minio/adapter.d.ts +1 -1
  234. package/dist/factory-assets/connectors/minio/adapter.d.ts.map +1 -1
  235. package/dist/factory-assets/connectors/minio.js +12 -12
  236. package/dist/factory-assets/connectors/postgres/adapter.d.ts +6 -1
  237. package/dist/factory-assets/connectors/postgres/adapter.d.ts.map +1 -1
  238. package/dist/factory-assets/connectors/postgres.js +12 -12
  239. package/dist/factory-assets/connectors/s3/driver.d.ts.map +1 -1
  240. package/dist/factory-assets/connectors/s3.js +12 -12
  241. package/dist/factory-assets/connectors/sharepoint.js +12 -12
  242. package/dist/factory-assets/connectors/sqlite/adapter.d.ts +1 -1
  243. package/dist/factory-assets/connectors/sqlite/adapter.d.ts.map +1 -1
  244. package/dist/factory-assets/connectors/sqlite.js +12 -12
  245. package/dist/factory-assets/connectors/static-server/adapter.d.ts +6 -1
  246. package/dist/factory-assets/connectors/static-server/adapter.d.ts.map +1 -1
  247. package/dist/factory-assets/connectors/static-server.js +12 -12
  248. package/dist/factory-assets/connectors/tunnel/adapter.d.ts +1 -1
  249. package/dist/factory-assets/connectors/tunnel/adapter.d.ts.map +1 -1
  250. package/dist/factory-assets/connectors/tunnel.js +12 -12
  251. package/dist/factory-assets/connectors/webdav.js +12 -12
  252. package/dist/factory-assets/connectors/xstate/adapter.d.ts +1 -1
  253. package/dist/factory-assets/connectors/xstate/adapter.d.ts.map +1 -1
  254. package/dist/factory-assets/connectors/xstate-store/adapter.d.ts +7 -0
  255. package/dist/factory-assets/connectors/xstate-store/adapter.d.ts.map +1 -1
  256. package/dist/factory-assets/connectors/xstate-store.js +12 -12
  257. package/dist/factory-assets/connectors/xstate.js +12 -12
  258. package/dist/flows-T6RCRKCX.js +7 -0
  259. package/dist/{flows-L3GHQD7F.js.map → flows-T6RCRKCX.js.map} +1 -1
  260. package/dist/library/index.js +13 -13
  261. package/dist/library/install.js +9 -9
  262. package/dist/library/src/config.d.ts +2 -2
  263. package/dist/library/src/config.d.ts.map +1 -1
  264. package/dist/library/src/index.d.ts +27 -27
  265. package/dist/library/src/index.d.ts.map +1 -1
  266. package/dist/library/src/install/index.d.ts +2 -2
  267. package/dist/library/src/install/index.d.ts.map +1 -1
  268. package/dist/library/src/install/install-from-manifest.d.ts +0 -21
  269. package/dist/library/src/install/install-from-manifest.d.ts.map +1 -1
  270. package/dist/library/src/knowledge/index.d.ts +2 -2
  271. package/dist/library/src/knowledge/index.d.ts.map +1 -1
  272. package/dist/library/src/knowledge/knowledge-kind-provider.d.ts.map +1 -1
  273. package/dist/library/src/local/library.d.ts +2 -2
  274. package/dist/library/src/local/library.d.ts.map +1 -1
  275. package/dist/library/src/local/store-paths.d.ts.map +1 -1
  276. package/dist/library/src/pin-resolver.d.ts.map +1 -1
  277. package/dist/library/src/preset/apply.d.ts +2 -2
  278. package/dist/library/src/preset/apply.d.ts.map +1 -1
  279. package/dist/library/src/preset/index.d.ts +10 -10
  280. package/dist/library/src/preset/index.d.ts.map +1 -1
  281. package/dist/library/src/preset/parse.d.ts.map +1 -1
  282. package/dist/library/src/preset/resolve-item.d.ts +1 -1
  283. package/dist/library/src/preset/resolve-item.d.ts.map +1 -1
  284. package/dist/library/src/preset/upgrade.d.ts +1 -1
  285. package/dist/library/src/preset/upgrade.d.ts.map +1 -1
  286. package/dist/library/src/remote/index.d.ts +5 -5
  287. package/dist/library/src/remote/index.d.ts.map +1 -1
  288. package/dist/library/src/remote/remote-catalog-source.d.ts +1 -1
  289. package/dist/library/src/remote/remote-catalog-source.d.ts.map +1 -1
  290. package/dist/library/src/remote/rest-catalog-source.d.ts +1 -1
  291. package/dist/library/src/remote/rest-catalog-source.d.ts.map +1 -1
  292. package/dist/library/src/sync/driver.d.ts +1 -1
  293. package/dist/library/src/sync/driver.d.ts.map +1 -1
  294. package/dist/library/src/sync/git-driver.d.ts +1 -1
  295. package/dist/library/src/sync/git-driver.d.ts.map +1 -1
  296. package/dist/library/src/sync/local-driver.d.ts +1 -1
  297. package/dist/library/src/sync/local-driver.d.ts.map +1 -1
  298. package/dist/library/src/workspace-config.d.ts +0 -9
  299. package/dist/library/src/workspace-config.d.ts.map +1 -1
  300. package/dist/open-library-ZXTSUPBJ.js +23 -0
  301. package/dist/{open-library-3VJY74LQ.js.map → open-library-ZXTSUPBJ.js.map} +1 -1
  302. package/dist/paths-F7RHQUFL.js +15 -0
  303. package/dist/{paths-CE66L3AL.js.map → paths-F7RHQUFL.js.map} +1 -1
  304. package/dist/plugin-registry/src/targets.d.ts +2 -3
  305. package/dist/plugin-registry/src/targets.d.ts.map +1 -1
  306. package/dist/{plugin-store-GL5VKYUA.js → plugin-store-BC3I36HA.js} +11 -14
  307. package/dist/plugin-store-BC3I36HA.js.map +1 -0
  308. package/dist/plugins/index.js +1 -1
  309. package/dist/plugins/src/asset-kind-registry.d.ts +1 -1
  310. package/dist/plugins/src/asset-kind-registry.d.ts.map +1 -1
  311. package/dist/plugins/src/catalog-source.d.ts +5 -0
  312. package/dist/plugins/src/catalog-source.d.ts.map +1 -1
  313. package/dist/plugins/src/index.d.ts +5 -5
  314. package/dist/plugins/src/index.d.ts.map +1 -1
  315. package/dist/{provider-GAWKFQ3T.js → provider-ZPT443IX.js} +18 -16
  316. package/dist/provider-ZPT443IX.js.map +1 -0
  317. package/dist/resolver/index.js +1 -1
  318. package/dist/resolver/src/preamble.d.ts.map +1 -1
  319. package/dist/runner/index.js +17 -17
  320. package/dist/runner/src/builtin-capabilities.d.ts +12 -0
  321. package/dist/runner/src/builtin-capabilities.d.ts.map +1 -1
  322. package/dist/runner/src/capability-registry.d.ts.map +1 -1
  323. package/dist/runner/src/compaction/index.d.ts +0 -4
  324. package/dist/runner/src/compaction/index.d.ts.map +1 -1
  325. package/dist/runner/src/compaction/orchestrator.d.ts +23 -2
  326. package/dist/runner/src/compaction/orchestrator.d.ts.map +1 -1
  327. package/dist/runner/src/composition/compile.d.ts.map +1 -1
  328. package/dist/runner/src/composition/index.d.ts +4 -4
  329. package/dist/runner/src/composition/index.d.ts.map +1 -1
  330. package/dist/runner/src/composition/resolve.d.ts.map +1 -1
  331. package/dist/runner/src/composition/types.d.ts +2 -2
  332. package/dist/runner/src/composition/types.d.ts.map +1 -1
  333. package/dist/runner/src/config.d.ts +2 -2
  334. package/dist/runner/src/config.d.ts.map +1 -1
  335. package/dist/runner/src/define-capability.d.ts +1 -1
  336. package/dist/runner/src/define-capability.d.ts.map +1 -1
  337. package/dist/runner/src/external-mcp.d.ts.map +1 -1
  338. package/dist/runner/src/index.d.ts +9 -9
  339. package/dist/runner/src/index.d.ts.map +1 -1
  340. package/dist/runner/src/logging-bootstrap.d.ts +1 -1
  341. package/dist/runner/src/logging-bootstrap.d.ts.map +1 -1
  342. package/dist/runner/src/manifest.d.ts.map +1 -1
  343. package/dist/runner/src/recipe-resolver.d.ts +32 -0
  344. package/dist/runner/src/recipe-resolver.d.ts.map +1 -1
  345. package/dist/runner/src/resource-handler.d.ts.map +1 -1
  346. package/dist/runner/src/resources.d.ts.map +1 -1
  347. package/dist/runner/src/runner-capabilities.d.ts +25 -0
  348. package/dist/runner/src/runner-capabilities.d.ts.map +1 -1
  349. package/dist/runner/src/serve.d.ts.map +1 -1
  350. package/dist/runner/src/session-builder.d.ts.map +1 -1
  351. package/dist/sdk/asset-manager.js +12 -12
  352. package/dist/sdk/bridge.js +4 -4
  353. package/dist/sdk/core.js +8 -8
  354. package/dist/sdk/flow.js +9 -9
  355. package/dist/sdk/index.js +92 -76
  356. package/dist/sdk/index.js.map +1 -1
  357. package/dist/sdk/resolver.js +1 -1
  358. package/dist/sdk/runner.js +17 -17
  359. package/dist/sdk/session.js +1 -1
  360. package/dist/sdk/src/agent-session.d.ts +8 -0
  361. package/dist/sdk/src/agent-session.d.ts.map +1 -1
  362. package/dist/sdk/src/factory.d.ts.map +1 -1
  363. package/dist/sdk/src/index.d.ts +2 -2
  364. package/dist/sdk/src/index.d.ts.map +1 -1
  365. package/dist/sdk/store.js +1 -1
  366. package/dist/sdk/telemetry.js +1 -1
  367. package/dist/secrets/index.js +2 -2
  368. package/dist/secrets/src/index.d.ts +8 -8
  369. package/dist/secrets/src/index.d.ts.map +1 -1
  370. package/dist/secrets/src/lifecycle.d.ts +1 -2
  371. package/dist/secrets/src/lifecycle.d.ts.map +1 -1
  372. package/dist/secrets/src/providers/env.d.ts +1 -1
  373. package/dist/secrets/src/providers/env.d.ts.map +1 -1
  374. package/dist/secrets/src/providers/local.d.ts +1 -1
  375. package/dist/secrets/src/providers/local.d.ts.map +1 -1
  376. package/dist/secrets/src/router.d.ts +1 -1
  377. package/dist/secrets/src/router.d.ts.map +1 -1
  378. package/dist/session/index.js +1 -1
  379. package/dist/session/src/mentions.d.ts.map +1 -1
  380. package/dist/{sessions-2IUX4P3P.js → sessions-DICDEONJ.js} +3 -3
  381. package/dist/{sessions-2IUX4P3P.js.map → sessions-DICDEONJ.js.map} +1 -1
  382. package/dist/{setup-4YMN2BDU.js → setup-FQ3DE6J6.js} +198 -175
  383. package/dist/setup-FQ3DE6J6.js.map +1 -0
  384. package/dist/skills-BMI6H7HI.js +7 -0
  385. package/dist/{skills-6HJ3WINN.js.map → skills-BMI6H7HI.js.map} +1 -1
  386. package/dist/store/index.js +1 -1
  387. package/dist/store/src/index.d.ts +1 -1
  388. package/dist/store/src/index.d.ts.map +1 -1
  389. package/dist/store/src/resource-client.d.ts +1 -1
  390. package/dist/store/src/resource-client.d.ts.map +1 -1
  391. package/dist/store/src/vue.d.ts.map +1 -1
  392. package/dist/store/vue.js.map +1 -1
  393. package/dist/store-client-27YYEWMM.js +15 -0
  394. package/dist/{store-client-BCP5LCAZ.js.map → store-client-27YYEWMM.js.map} +1 -1
  395. package/dist/telemetry/index.js +1 -1
  396. package/dist/telemetry/src/context.d.ts.map +1 -1
  397. package/dist/telemetry/src/index.d.ts +3 -3
  398. package/dist/telemetry/src/index.d.ts.map +1 -1
  399. package/dist/telemetry/src/otel/gen-ai.d.ts +1 -1
  400. package/dist/telemetry/src/otel/gen-ai.d.ts.map +1 -1
  401. package/dist/telemetry/src/otel/provider.d.ts.map +1 -1
  402. package/dist/transport/src/index.d.ts +1 -1
  403. package/dist/transport/src/index.d.ts.map +1 -1
  404. package/dist/tui/index.js +251 -218
  405. package/dist/tui/index.js.map +1 -1
  406. package/dist/tui/src/index.d.ts +2 -2
  407. package/dist/tui/src/index.d.ts.map +1 -1
  408. package/dist/tui/src/repl-renderer.d.ts.map +1 -1
  409. package/dist/tui/src/repl-session.d.ts +1 -1
  410. package/dist/tui/src/repl-session.d.ts.map +1 -1
  411. package/dist/tui/src/repl.d.ts.map +1 -1
  412. package/dist/types/manifests.js +1 -1
  413. package/dist/types/src/envelope.d.ts +1 -1
  414. package/dist/types/src/envelope.d.ts.map +1 -1
  415. package/dist/types/src/manifests/agent.d.ts.map +1 -1
  416. package/dist/types/src/manifests/connector.d.ts.map +1 -1
  417. package/dist/types/src/manifests/contract.d.ts.map +1 -1
  418. package/dist/types/src/manifests/index.d.ts +16 -16
  419. package/dist/types/src/manifests/index.d.ts.map +1 -1
  420. package/dist/types/src/manifests/mcp-server.d.ts.map +1 -1
  421. package/dist/types/src/manifests/mount.d.ts.map +1 -1
  422. package/dist/types/src/manifests/persona.d.ts.map +1 -1
  423. package/dist/types/src/manifests/preset.d.ts +0 -18
  424. package/dist/types/src/manifests/preset.d.ts.map +1 -1
  425. package/dist/types/src/manifests/prompt.d.ts.map +1 -1
  426. package/dist/types/src/manifests/ruleset.d.ts.map +1 -1
  427. package/dist/types/src/manifests/skill.d.ts.map +1 -1
  428. package/dist/validator-VF5M764T.js +5 -0
  429. package/dist/{validator-47SQUW3J.js.map → validator-VF5M764T.js.map} +1 -1
  430. package/dist/workspace-plugin/adapters/mcp.js +6 -6
  431. package/dist/workspace-plugin/adapters/omp.js +76 -69
  432. package/dist/workspace-plugin/adapters/omp.js.map +1 -1
  433. package/dist/workspace-plugin/index.js +2 -2
  434. package/dist/workspace-plugin/src/adapters/omp.d.ts.map +1 -1
  435. package/dist/workspace-plugin/src/tools/connectors.d.ts.map +1 -1
  436. package/dist/workspace-plugin/src/tools/flows.d.ts.map +1 -1
  437. package/dist/workspace-plugin/src/tools/sessions.d.ts.map +1 -1
  438. package/dist/workspace-plugin/src/tools/skills.d.ts.map +1 -1
  439. package/dist/workspace-plugin/src/tools/validator.d.ts.map +1 -1
  440. package/package.json +1 -2
  441. package/dist/asset-feeds-2V7LZIG7.js.map +0 -1
  442. package/dist/chunk-24UIWON4.js.map +0 -1
  443. package/dist/chunk-2FS4FPZ7.js.map +0 -1
  444. package/dist/chunk-3QQSSRYN.js.map +0 -1
  445. package/dist/chunk-5YO3H5Q4.js.map +0 -1
  446. package/dist/chunk-7GWYRVAX.js.map +0 -1
  447. package/dist/chunk-AKSR434V.js.map +0 -1
  448. package/dist/chunk-DVMSGGCM.js.map +0 -1
  449. package/dist/chunk-FQHHCICT.js.map +0 -1
  450. package/dist/chunk-IEJ5IOC3.js.map +0 -1
  451. package/dist/chunk-IERB3UXJ.js.map +0 -1
  452. package/dist/chunk-JIQLLZ65.js.map +0 -1
  453. package/dist/chunk-M4KLIEIJ.js.map +0 -1
  454. package/dist/chunk-MIH45XPY.js.map +0 -1
  455. package/dist/chunk-MLJKA3S6.js.map +0 -1
  456. package/dist/chunk-OTFG5Q7H.js.map +0 -1
  457. package/dist/chunk-PAYAFNXG.js.map +0 -1
  458. package/dist/chunk-QDJKE557.js.map +0 -1
  459. package/dist/chunk-SF4GWVR5.js.map +0 -1
  460. package/dist/chunk-TV72ZJYN.js.map +0 -1
  461. package/dist/chunk-UIZKIUC6.js.map +0 -1
  462. package/dist/chunk-W7W66MDZ.js.map +0 -1
  463. package/dist/chunk-Z3XXKR5E.js.map +0 -1
  464. package/dist/chunk-ZONCCO5N.js.map +0 -1
  465. package/dist/ensure-sources-BNSRO5G7.js +0 -74
  466. package/dist/ensure-sources-BNSRO5G7.js.map +0 -1
  467. package/dist/flows-L3GHQD7F.js +0 -7
  468. package/dist/open-library-3VJY74LQ.js +0 -23
  469. package/dist/paths-CE66L3AL.js +0 -15
  470. package/dist/plugin-store-GL5VKYUA.js.map +0 -1
  471. package/dist/provider-GAWKFQ3T.js.map +0 -1
  472. package/dist/setup-4YMN2BDU.js.map +0 -1
  473. package/dist/skills-6HJ3WINN.js +0 -7
  474. package/dist/store-client-BCP5LCAZ.js +0 -15
  475. package/dist/validator-47SQUW3J.js +0 -5
@@ -1,15 +1,15 @@
1
- import { readManifestSourceConfig, discoverAssetsInTree } from './chunk-JIQLLZ65.js';
2
- import { installFromManifest } from './chunk-Z3XXKR5E.js';
3
- import { parseAssetRef } from './chunk-IEJ5IOC3.js';
4
- import { PresetManifestSchema } from './chunk-QDJKE557.js';
1
+ import { readManifestSourceConfig, discoverAssetsInTree } from './chunk-7MANNJ3U.js';
2
+ import { installFromManifest } from './chunk-WDMH4UMD.js';
3
+ import { parseAssetRef } from './chunk-LFDZYCEO.js';
4
+ import { PresetManifestSchema } from './chunk-HRKB2ATT.js';
5
5
  import { __export } from './chunk-NSBPE2FW.js';
6
- import * as crypto from 'crypto';
7
- import * as fs11 from 'fs';
8
- import * as path4 from 'path';
9
- import { eq, like, and } from 'drizzle-orm';
10
- import * as z2 from 'zod';
6
+ import * as fs10 from 'fs';
11
7
  import * as os from 'os';
8
+ import * as path5 from 'path';
12
9
  import { stringify, parse, parseDocument } from 'yaml';
10
+ import * as z3 from 'zod';
11
+ import * as crypto from 'crypto';
12
+ import { eq, like, and } from 'drizzle-orm';
13
13
  import { createClient } from '@libsql/client';
14
14
  import { drizzle } from 'drizzle-orm/libsql';
15
15
  import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
@@ -74,46 +74,48 @@ var InstanceHasConsumersError = class extends LibraryError {
74
74
  this.name = "InstanceHasConsumersError";
75
75
  }
76
76
  };
77
+
78
+ // library/src/config.ts
77
79
  var LOCAL_CATALOG_URL = "local";
78
80
  function isLocalCatalogUrl(url) {
79
81
  return typeof url === "string" && url.trim().toLowerCase() === LOCAL_CATALOG_URL;
80
82
  }
81
- var CatalogConfigSchema = z2.object({
83
+ var CatalogConfigSchema = z3.object({
82
84
  /** Catalog base URL, or `"local"` (case-insensitive) to disable the remote catalog. Default: https://skaile.store */
83
- url: z2.union([
84
- z2.string().url(),
85
+ url: z3.union([
86
+ z3.string().url(),
85
87
  // Accept any casing/whitespace variant of "local", normalise to canonical.
86
- z2.string().refine((s) => s.trim().toLowerCase() === LOCAL_CATALOG_URL, {
88
+ z3.string().refine((s) => s.trim().toLowerCase() === LOCAL_CATALOG_URL, {
87
89
  message: `Expected a valid URL or "${LOCAL_CATALOG_URL}"`
88
90
  }).transform(() => LOCAL_CATALOG_URL)
89
91
  ]).optional(),
90
92
  /** Cache TTL in seconds. 0 = air-gapped (manual `skaile update` only). */
91
- cache_ttl: z2.number().int().min(0).optional(),
93
+ cache_ttl: z3.number().int().min(0).optional(),
92
94
  /**
93
95
  * Wire framing to use when talking to a remote catalog.
94
96
  * `"trpc"` — skaile.store tRPC backend (default for `https://skaile.store`).
95
97
  * `"rest"` — forge-store REST backend (default for any other URL).
96
98
  */
97
- framing: z2.enum(["trpc", "rest"]).optional()
99
+ framing: z3.enum(["trpc", "rest"]).optional()
98
100
  });
99
- var LibraryConfigSchema = z2.object({
101
+ var LibraryConfigSchema = z3.object({
100
102
  /** Library storage directory. Default: ~/.skaile/libraries */
101
- dir: z2.string().optional(),
103
+ dir: z3.string().optional(),
102
104
  /** Default pin policy for new instances/assignments. */
103
- pin_policy: z2.enum(PIN_POLICIES).optional()
105
+ pin_policy: z3.enum(PIN_POLICIES).optional()
104
106
  });
105
- var SkaileConfigSchema = z2.object({
107
+ var SkaileConfigSchema = z3.object({
106
108
  catalog: CatalogConfigSchema.optional(),
107
109
  library: LibraryConfigSchema.optional()
108
110
  });
109
111
  function skaileHomeDir() {
110
- return path4.join(os.homedir(), ".skaile");
112
+ return path5.join(os.homedir(), ".skaile");
111
113
  }
112
114
  function defaultLibraryDir() {
113
- return path4.join(skaileHomeDir(), "libraries");
115
+ return path5.join(skaileHomeDir(), "libraries");
114
116
  }
115
117
  function defaultIndexPath() {
116
- return path4.join(skaileHomeDir(), "index.db");
118
+ return path5.join(skaileHomeDir(), "index.db");
117
119
  }
118
120
  function resolveLibraryDir() {
119
121
  if (process.env.SKAILE_LIBRARIES_DIR) return process.env.SKAILE_LIBRARIES_DIR;
@@ -131,15 +133,15 @@ function getConfigDefaults() {
131
133
  }
132
134
  var CONFIG_FILENAME = "config.yaml";
133
135
  function userConfigPath() {
134
- return path4.join(os.homedir(), ".skaile", CONFIG_FILENAME);
136
+ return path5.join(os.homedir(), ".skaile", CONFIG_FILENAME);
135
137
  }
136
138
  function projectConfigPath(projectDir) {
137
- return path4.join(projectDir, ".skaile", CONFIG_FILENAME);
139
+ return path5.join(projectDir, ".skaile", CONFIG_FILENAME);
138
140
  }
139
141
  function loadConfigFile(filePath) {
140
- if (!fs11.existsSync(filePath)) return null;
142
+ if (!fs10.existsSync(filePath)) return null;
141
143
  try {
142
- const content = fs11.readFileSync(filePath, "utf-8");
144
+ const content = fs10.readFileSync(filePath, "utf-8");
143
145
  const raw = parse(content);
144
146
  if (!raw || typeof raw !== "object") return null;
145
147
  return SkaileConfigSchema.partial().parse(raw);
@@ -187,12 +189,76 @@ function resolveConfig(opts) {
187
189
  return resolved;
188
190
  }
189
191
  function saveConfig(filePath, config) {
190
- const dir = path4.dirname(filePath);
191
- if (!fs11.existsSync(dir)) {
192
- fs11.mkdirSync(dir, { recursive: true });
192
+ const dir = path5.dirname(filePath);
193
+ if (!fs10.existsSync(dir)) {
194
+ fs10.mkdirSync(dir, { recursive: true });
195
+ }
196
+ fs10.writeFileSync(filePath, stringify(config, { indent: 2 }), "utf-8");
197
+ }
198
+ var KnowledgeManifestSchema = z3.looseObject({
199
+ name: z3.string().optional(),
200
+ description: z3.string().optional(),
201
+ version: z3.string().optional(),
202
+ files: z3.array(z3.string()).optional(),
203
+ keywords: z3.array(z3.string()).optional(),
204
+ license: z3.string().optional()
205
+ });
206
+ function validateKnowledge(data) {
207
+ const result = KnowledgeManifestSchema.safeParse(data);
208
+ if (result.success) {
209
+ return { ok: true, data: result.data };
210
+ }
211
+ return {
212
+ ok: false,
213
+ errors: result.error.issues.map((issue) => ({
214
+ path: issue.path.join("."),
215
+ message: issue.message
216
+ }))
217
+ };
218
+ }
219
+
220
+ // library/src/knowledge/knowledge-kind-provider.ts
221
+ function collectDirRecursive(baseDir, relativeDir) {
222
+ const full = path5.join(baseDir, relativeDir);
223
+ if (!fs10.existsSync(full) || !fs10.statSync(full).isDirectory()) return [];
224
+ const results = [];
225
+ for (const entry of fs10.readdirSync(full, { withFileTypes: true })) {
226
+ const rel = path5.posix.join(relativeDir, entry.name);
227
+ if (entry.isDirectory()) {
228
+ results.push(...collectDirRecursive(baseDir, rel));
229
+ } else if (entry.isFile()) {
230
+ results.push(rel);
231
+ }
193
232
  }
194
- fs11.writeFileSync(filePath, stringify(config, { indent: 2 }), "utf-8");
233
+ return results;
195
234
  }
235
+ var knowledgeKindProvider = {
236
+ kind: "knowledge",
237
+ displayName: "Knowledge",
238
+ providerVersion: "1.0.0",
239
+ discoveryPatterns: ["**/knowledge.yaml", "**/knowledge/index.yaml"],
240
+ matchesPath(relativePath) {
241
+ const parts = relativePath.split("/");
242
+ const filename = parts[parts.length - 1] ?? "";
243
+ if (filename === "knowledge.yaml") return true;
244
+ if (filename === "index.yaml" && parts.length >= 2 && parts[parts.length - 2] === "knowledge") {
245
+ return true;
246
+ }
247
+ return false;
248
+ },
249
+ validateManifest(data) {
250
+ return validateKnowledge(data);
251
+ },
252
+ defaultFileFilter(manifestPath) {
253
+ const dir = path5.dirname(manifestPath);
254
+ const manifestName = path5.basename(manifestPath);
255
+ const files = [manifestName];
256
+ files.push(...collectDirRecursive(dir, "documents"));
257
+ files.sort();
258
+ return { assetRoot: dir, files };
259
+ }
260
+ // Knowledge has no cross-asset dependency edges — omit extractRequires
261
+ };
196
262
 
197
263
  // library/src/local/schema.ts
198
264
  var schema_exports = {};
@@ -265,23 +331,23 @@ var assignments = sqliteTable("assignments", {
265
331
  });
266
332
  var SKAILE_HOME_ENV = "SKAILE_HOME";
267
333
  function getSidecarRoot() {
268
- const home = process.env[SKAILE_HOME_ENV] ?? path4.join(os.homedir(), ".skaile");
269
- return path4.join(home, "sources");
334
+ const home = process.env[SKAILE_HOME_ENV] ?? path5.join(os.homedir(), ".skaile");
335
+ return path5.join(home, "sources");
270
336
  }
271
337
  function resolveSidecarPaths(slug) {
272
338
  const rootDir = getSidecarRoot();
273
- const sidecarDir = path4.join(rootDir, slug);
339
+ const sidecarDir = path5.join(rootDir, slug);
274
340
  return {
275
341
  rootDir,
276
342
  sidecarDir,
277
- manifestFile: path4.join(sidecarDir, "skaile.manifest.yaml"),
278
- readmeFile: path4.join(sidecarDir, "README.md"),
279
- lockFile: path4.join(sidecarDir, "skaile.manifest.lock.json")
343
+ manifestFile: path5.join(sidecarDir, "skaile.manifest.yaml"),
344
+ readmeFile: path5.join(sidecarDir, "README.md"),
345
+ lockFile: path5.join(sidecarDir, "skaile.manifest.lock.json")
280
346
  };
281
347
  }
282
348
  function deriveSlug(sourceConfig, sourcePath, existingSlugs) {
283
349
  const fromConfig = sourceConfig && typeof sourceConfig.name === "string" ? sourceConfig.name : void 0;
284
- const base = fromConfig && fromConfig.length > 0 ? slugify(fromConfig) : slugify(path4.basename(sourcePath || "")) || "unnamed-source";
350
+ const base = fromConfig && fromConfig.length > 0 ? slugify(fromConfig) : slugify(path5.basename(sourcePath || "")) || "unnamed-source";
285
351
  const taken = new Set(existingSlugs);
286
352
  if (!taken.has(base)) return base;
287
353
  for (let i = 2; i < 1e3; i++) {
@@ -292,9 +358,9 @@ function deriveSlug(sourceConfig, sourcePath, existingSlugs) {
292
358
  }
293
359
  function listSidecarSlugsOnDisk() {
294
360
  const root = getSidecarRoot();
295
- if (!fs11.existsSync(root)) return [];
361
+ if (!fs10.existsSync(root)) return [];
296
362
  const out = [];
297
- for (const entry of fs11.readdirSync(root, { withFileTypes: true })) {
363
+ for (const entry of fs10.readdirSync(root, { withFileTypes: true })) {
298
364
  if (!entry.isDirectory()) continue;
299
365
  if (entry.name.startsWith(".")) continue;
300
366
  out.push(entry.name);
@@ -307,40 +373,37 @@ function slugify(input) {
307
373
 
308
374
  // library/src/local/store-paths.ts
309
375
  function getStoreRoot() {
310
- const home = process.env.SKAILE_HOME ?? path4.join(os.homedir(), ".skaile");
311
- return path4.join(home, "store");
376
+ const home = process.env.SKAILE_HOME ?? path5.join(os.homedir(), ".skaile");
377
+ return path5.join(home, "store");
312
378
  }
313
379
  function getStoreManifestsDir() {
314
- return path4.join(getStoreRoot(), "manifests");
380
+ return path5.join(getStoreRoot(), "manifests");
315
381
  }
316
382
  function migratedMarkerPath() {
317
- return path4.join(getStoreRoot(), ".migrated");
383
+ return path5.join(getStoreRoot(), ".migrated");
318
384
  }
319
- function migrateSidecarsToStoreIfNeeded() {
320
- const storeRoot = getStoreRoot();
321
- const marker = migratedMarkerPath();
322
- if (fs11.existsSync(marker)) return { migrated: 0, storeRoot };
323
- const sidecarRoot = getSidecarRoot();
324
- const manifestsDir = getStoreManifestsDir();
385
+ function collectSidecarMigrationCandidates(sidecarRoot) {
325
386
  const candidates = [];
326
- if (fs11.existsSync(sidecarRoot)) {
327
- for (const entry of fs11.readdirSync(sidecarRoot, { withFileTypes: true })) {
387
+ if (fs10.existsSync(sidecarRoot)) {
388
+ for (const entry of fs10.readdirSync(sidecarRoot, { withFileTypes: true })) {
328
389
  if (!entry.isDirectory()) continue;
329
390
  if (entry.name.startsWith(".")) continue;
330
- const sidecarDir = path4.join(sidecarRoot, entry.name);
331
- const current = path4.join(sidecarDir, "skaile.manifest.yaml");
332
- const legacy = path4.join(sidecarDir, ".skaile-source.yaml");
333
- const src = fs11.existsSync(current) ? current : fs11.existsSync(legacy) ? legacy : void 0;
391
+ const sidecarDir = path5.join(sidecarRoot, entry.name);
392
+ const current = path5.join(sidecarDir, "skaile.manifest.yaml");
393
+ const legacy = path5.join(sidecarDir, ".skaile-source.yaml");
394
+ const src = fs10.existsSync(current) ? current : fs10.existsSync(legacy) ? legacy : void 0;
334
395
  if (src) candidates.push({ src, slug: entry.name });
335
396
  }
336
397
  }
337
- fs11.mkdirSync(manifestsDir, { recursive: true });
398
+ return candidates;
399
+ }
400
+ function moveSidecarManifests(candidates, manifestsDir) {
338
401
  let migrated = 0;
339
402
  for (const { src, slug } of candidates) {
340
- const dest = path4.join(manifestsDir, `${slug}.yaml`);
341
- if (fs11.existsSync(dest)) continue;
403
+ const dest = path5.join(manifestsDir, `${slug}.yaml`);
404
+ if (fs10.existsSync(dest)) continue;
342
405
  try {
343
- fs11.renameSync(src, dest);
406
+ fs10.renameSync(src, dest);
344
407
  migrated++;
345
408
  } catch (err) {
346
409
  process.stderr.write(
@@ -349,18 +412,31 @@ function migrateSidecarsToStoreIfNeeded() {
349
412
  );
350
413
  }
351
414
  }
352
- if (!fs11.existsSync(path4.join(storeRoot, ".git"))) {
353
- try {
354
- spawnSync("git", ["init", "--quiet"], { cwd: storeRoot, stdio: "ignore" });
355
- fs11.writeFileSync(
356
- path4.join(storeRoot, ".gitignore"),
357
- "# Remote-store cache \u2014 local to this machine.\nremote-cache/\n",
358
- "utf8"
359
- );
360
- } catch {
361
- }
415
+ return migrated;
416
+ }
417
+ function initStoreGitRepo(storeRoot) {
418
+ if (fs10.existsSync(path5.join(storeRoot, ".git"))) return;
419
+ try {
420
+ spawnSync("git", ["init", "--quiet"], { cwd: storeRoot, stdio: "ignore" });
421
+ fs10.writeFileSync(
422
+ path5.join(storeRoot, ".gitignore"),
423
+ "# Remote-store cache \u2014 local to this machine.\nremote-cache/\n",
424
+ "utf8"
425
+ );
426
+ } catch {
362
427
  }
363
- fs11.writeFileSync(marker, (/* @__PURE__ */ new Date()).toISOString(), "utf8");
428
+ }
429
+ function migrateSidecarsToStoreIfNeeded() {
430
+ const storeRoot = getStoreRoot();
431
+ const marker = migratedMarkerPath();
432
+ if (fs10.existsSync(marker)) return { migrated: 0, storeRoot };
433
+ const sidecarRoot = getSidecarRoot();
434
+ const manifestsDir = getStoreManifestsDir();
435
+ const candidates = collectSidecarMigrationCandidates(sidecarRoot);
436
+ fs10.mkdirSync(manifestsDir, { recursive: true });
437
+ const migrated = moveSidecarManifests(candidates, manifestsDir);
438
+ initStoreGitRepo(storeRoot);
439
+ fs10.writeFileSync(marker, (/* @__PURE__ */ new Date()).toISOString(), "utf8");
364
440
  return { migrated, storeRoot };
365
441
  }
366
442
 
@@ -441,7 +517,7 @@ async function librariesPresent(client) {
441
517
  return rs.rows.length > 0;
442
518
  }
443
519
  function slugify2(p) {
444
- const base = path4.basename(p) || "lib";
520
+ const base = path5.basename(p) || "lib";
445
521
  return base.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-|-$/g, "") || "lib";
446
522
  }
447
523
  async function runLibrariesMigration(client) {
@@ -527,23 +603,23 @@ async function runAssignmentsMigration(client) {
527
603
  ]);
528
604
  }
529
605
  function assertNoLegacyLibDb(newDbPath) {
530
- if (fs11.existsSync(newDbPath)) return;
531
- const parent = path4.dirname(newDbPath);
532
- const skaileHome = path4.join(os.homedir(), ".skaile");
533
- const defaultIndexPath2 = path4.join(skaileHome, "index.db");
606
+ if (fs10.existsSync(newDbPath)) return;
607
+ const parent = path5.dirname(newDbPath);
608
+ const skaileHome = path5.join(os.homedir(), ".skaile");
609
+ const defaultIndexPath2 = path5.join(skaileHome, "index.db");
534
610
  const candidates = [];
535
- if (path4.basename(newDbPath) === "index.db") {
536
- candidates.push(path4.join(parent, "lib.db"));
611
+ if (path5.basename(newDbPath) === "index.db") {
612
+ candidates.push(path5.join(parent, "lib.db"));
537
613
  }
538
614
  if (newDbPath === defaultIndexPath2) {
539
- candidates.push(path4.join(skaileHome, "libraries", "lib.db"));
540
- candidates.push(path4.join(skaileHome, "library", "lib.db"));
615
+ candidates.push(path5.join(skaileHome, "libraries", "lib.db"));
616
+ candidates.push(path5.join(skaileHome, "library", "lib.db"));
541
617
  }
542
- if (path4.basename(parent) === "libraries") {
543
- candidates.push(path4.join(path4.dirname(parent), "library", "lib.db"));
618
+ if (path5.basename(parent) === "libraries") {
619
+ candidates.push(path5.join(path5.dirname(parent), "library", "lib.db"));
544
620
  }
545
621
  for (const legacy of candidates) {
546
- if (!fs11.existsSync(legacy)) continue;
622
+ if (!fs10.existsSync(legacy)) continue;
547
623
  throw new Error(
548
624
  `Found legacy index database at ${legacy}. The lib.db \u2192 index.db relocation ran for one release and has been removed. Move the file manually to ${newDbPath} (and any -wal / -shm siblings), then retry.`
549
625
  );
@@ -683,26 +759,26 @@ var LocalIndex = class {
683
759
  let dbPath;
684
760
  if (typeof optionsOrDir === "string") {
685
761
  dir = optionsOrDir;
686
- dbPath = path4.join(dir, "index.db");
762
+ dbPath = path5.join(dir, "index.db");
687
763
  } else if (optionsOrDir?.indexPath) {
688
764
  dbPath = optionsOrDir.indexPath;
689
765
  dir = optionsOrDir.libraryDir ?? resolveLibraryDir();
690
766
  this._kindRegistry = optionsOrDir.kindRegistry;
691
767
  } else if (optionsOrDir?.libraryDir) {
692
768
  dir = optionsOrDir.libraryDir;
693
- dbPath = path4.join(dir, "index.db");
769
+ dbPath = path5.join(dir, "index.db");
694
770
  this._kindRegistry = optionsOrDir.kindRegistry;
695
771
  } else {
696
772
  dir = resolveLibraryDir();
697
773
  dbPath = resolveIndexPath();
698
774
  this._kindRegistry = optionsOrDir?.kindRegistry;
699
775
  }
700
- if (!fs11.existsSync(dir)) {
701
- fs11.mkdirSync(dir, { recursive: true });
776
+ if (!fs10.existsSync(dir)) {
777
+ fs10.mkdirSync(dir, { recursive: true });
702
778
  }
703
- const dbParent = path4.dirname(dbPath);
704
- if (!fs11.existsSync(dbParent)) {
705
- fs11.mkdirSync(dbParent, { recursive: true });
779
+ const dbParent = path5.dirname(dbPath);
780
+ if (!fs10.existsSync(dbParent)) {
781
+ fs10.mkdirSync(dbParent, { recursive: true });
706
782
  }
707
783
  this.libraryDir = dir;
708
784
  this.dbPath = dbPath;
@@ -900,7 +976,7 @@ var LocalIndex = class {
900
976
  * @returns The created Instance plus its on-disk install path.
901
977
  */
902
978
  async install(manifest, opts) {
903
- const libraryRoot = opts?.libraryRoot ?? path4.join(this.libraryDir, "installed");
979
+ const libraryRoot = opts?.libraryRoot ?? path5.join(this.libraryDir, "installed");
904
980
  const { installPath } = await installFromManifest({
905
981
  manifest,
906
982
  libraryRoot,
@@ -1329,8 +1405,8 @@ var LibraryManager = class {
1329
1405
  const lib = await this.requireLibrary(nameOrId);
1330
1406
  const db = await this.lib.dbHandle();
1331
1407
  await db.delete(libraries).where(eq(libraries.id, lib.id)).run();
1332
- if (opts.purge && fs11.existsSync(lib.path)) {
1333
- fs11.rmSync(lib.path, { recursive: true, force: true });
1408
+ if (opts.purge && fs10.existsSync(lib.path)) {
1409
+ fs10.rmSync(lib.path, { recursive: true, force: true });
1334
1410
  }
1335
1411
  }
1336
1412
  /**
@@ -1346,8 +1422,8 @@ var LibraryManager = class {
1346
1422
  (l) => l.isDefault && (l.ownership === "owner" || l.ownership === "contributor")
1347
1423
  );
1348
1424
  if (writableDefault) return { library: writableDefault, created: false };
1349
- const personalPath = path4.join(librariesDir, "personal");
1350
- fs11.mkdirSync(personalPath, { recursive: true });
1425
+ const personalPath = path5.join(librariesDir, "personal");
1426
+ fs10.mkdirSync(personalPath, { recursive: true });
1351
1427
  const lib = await this.addLibrary({
1352
1428
  name: "personal",
1353
1429
  path: personalPath,
@@ -1365,6 +1441,121 @@ var LibraryManager = class {
1365
1441
  return { library: lib, created: true };
1366
1442
  }
1367
1443
  };
1444
+
1445
+ // library/src/local/local-catalog-source.ts
1446
+ var LocalCatalogSource = class {
1447
+ constructor(library, sourceId, rootPath, registry, options) {
1448
+ this.library = library;
1449
+ this.sourceId = sourceId;
1450
+ this.rootPath = rootPath;
1451
+ this.registry = registry;
1452
+ this.id = `local:${sourceId}`;
1453
+ this.sidecarPath = options?.sidecarPath;
1454
+ }
1455
+ library;
1456
+ sourceId;
1457
+ rootPath;
1458
+ registry;
1459
+ id;
1460
+ sidecarPath;
1461
+ async resolve(ref) {
1462
+ const def = await this.library.getAssetDef(ref);
1463
+ if (!def || def.libraryId !== this.sourceId) return null;
1464
+ return defToCatalogAsset(def);
1465
+ }
1466
+ async listAssets(filter) {
1467
+ const libraryFilter = { libraryId: this.sourceId };
1468
+ if (filter?.kind) libraryFilter.kind = filter.kind;
1469
+ if (filter?.publisher) libraryFilter.publisher = filter.publisher;
1470
+ if (filter?.prefix) libraryFilter.prefix = filter.prefix;
1471
+ const defs = await this.library.listAssetDefs(libraryFilter);
1472
+ return defs.map(defToCatalogAsset);
1473
+ }
1474
+ async listVersions(ref) {
1475
+ const parsed = parseAssetRef(ref);
1476
+ const defs = await this.library.listAssetDefs({
1477
+ libraryId: this.sourceId,
1478
+ kind: parsed.kind
1479
+ });
1480
+ const wantPublisher = parsed.publisher?.replace(/^@/, "");
1481
+ return defs.filter(
1482
+ (d) => d.name === parsed.name && (wantPublisher === void 0 || d.publisher.replace(/^@/, "") === wantPublisher)
1483
+ ).map((d) => d.version);
1484
+ }
1485
+ async fetchTarball(_ref, _sha256) {
1486
+ throw new Error(
1487
+ "LocalCatalogSource does not support tarball delivery. Assets are available on disk."
1488
+ );
1489
+ }
1490
+ async listSources() {
1491
+ return [{ id: this.sourceId, type: "local", url: this.rootPath }];
1492
+ }
1493
+ /**
1494
+ * Sync: discover assets in the source directory and cache them in the library.
1495
+ *
1496
+ * Runs the discovery pipeline (glob -> parse -> filter -> hash -> requires)
1497
+ * and upserts each discovered asset into the LocalIndex cache.
1498
+ *
1499
+ * @param syncOpts - When `includeDev` is set, assets under the source
1500
+ * config's `dev_paths` (e.g. `ai-assets-dev/`) are discovered and cached
1501
+ * too. Defaults to excluding them (a normal run).
1502
+ * @returns Discovery statistics and any errors encountered.
1503
+ */
1504
+ async sync(syncOpts) {
1505
+ const opts = {
1506
+ registry: this.registry,
1507
+ sidecarPath: this.sidecarPath,
1508
+ includeDev: syncOpts?.includeDev ?? false
1509
+ };
1510
+ if (!this.sidecarPath) {
1511
+ opts.sourceConfig = readManifestSourceConfig(this.rootPath);
1512
+ }
1513
+ const result = discoverAssetsInTree(this.rootPath, opts);
1514
+ let assetsUpdated = 0;
1515
+ const errors = [];
1516
+ for (const asset of result.assets) {
1517
+ if (asset.kind === "preset") continue;
1518
+ try {
1519
+ await this.library.upsertAssetDef({
1520
+ id: asset.ref,
1521
+ kind: asset.kind,
1522
+ publisher: asset.publisher,
1523
+ name: asset.name,
1524
+ version: asset.version,
1525
+ sha256: asset.sha256,
1526
+ description: typeof asset.manifest.description === "string" ? asset.manifest.description : void 0,
1527
+ license: typeof asset.manifest.license === "string" ? asset.manifest.license : void 0,
1528
+ category: typeof asset.manifest.category === "string" ? asset.manifest.category : void 0,
1529
+ manifest: asset.manifest,
1530
+ libraryId: this.sourceId,
1531
+ cachedAt: /* @__PURE__ */ new Date(),
1532
+ updatedAt: /* @__PURE__ */ new Date()
1533
+ });
1534
+ assetsUpdated++;
1535
+ } catch (err) {
1536
+ errors.push(`${asset.ref}: ${err instanceof Error ? err.message : String(err)}`);
1537
+ }
1538
+ }
1539
+ for (const err of result.errors) {
1540
+ errors.push(`${err.path}: ${err.error}`);
1541
+ }
1542
+ return { assetsFound: result.assets.length, assetsUpdated, errors };
1543
+ }
1544
+ };
1545
+ function defToCatalogAsset(def) {
1546
+ return {
1547
+ id: def.id,
1548
+ kind: def.kind,
1549
+ publisher: def.publisher,
1550
+ name: def.name,
1551
+ version: def.version,
1552
+ sha256: def.sha256,
1553
+ description: def.description,
1554
+ license: def.license,
1555
+ category: def.category,
1556
+ manifest: def.manifest
1557
+ };
1558
+ }
1368
1559
  var FALLBACK_USER_NAME = "Skaile CLI";
1369
1560
  var FALLBACK_USER_EMAIL = "skaile-cli@local";
1370
1561
  var ROOT_README = `# Skaile Sidecar Manifests
@@ -1409,21 +1600,21 @@ var SidecarGit = class {
1409
1600
  * @docLink packages/library/concepts#sidecar-git
1410
1601
  */
1411
1602
  async ensureInitialized() {
1412
- if (!fs11.existsSync(this.rootDir)) {
1413
- fs11.mkdirSync(this.rootDir, { recursive: true });
1603
+ if (!fs10.existsSync(this.rootDir)) {
1604
+ fs10.mkdirSync(this.rootDir, { recursive: true });
1414
1605
  }
1415
- const gitDir = path4.join(this.rootDir, ".git");
1416
- const freshRepo = !fs11.existsSync(gitDir);
1606
+ const gitDir = path5.join(this.rootDir, ".git");
1607
+ const freshRepo = !fs10.existsSync(gitDir);
1417
1608
  if (freshRepo) {
1418
1609
  const init = this.spawn(["init", "-b", "main"]);
1419
1610
  if (init.exitCode !== 0) {
1420
1611
  throw new Error(`git init failed: ${init.stderr || init.stdout}`);
1421
1612
  }
1422
1613
  }
1423
- const attrsPath = path4.join(this.rootDir, ".gitattributes");
1424
- if (!fs11.existsSync(attrsPath)) fs11.writeFileSync(attrsPath, GITATTRIBUTES, "utf-8");
1425
- const readmePath = path4.join(this.rootDir, "README.md");
1426
- if (!fs11.existsSync(readmePath)) fs11.writeFileSync(readmePath, ROOT_README, "utf-8");
1614
+ const attrsPath = path5.join(this.rootDir, ".gitattributes");
1615
+ if (!fs10.existsSync(attrsPath)) fs10.writeFileSync(attrsPath, GITATTRIBUTES, "utf-8");
1616
+ const readmePath = path5.join(this.rootDir, "README.md");
1617
+ if (!fs10.existsSync(readmePath)) fs10.writeFileSync(readmePath, ROOT_README, "utf-8");
1427
1618
  if (!this.hasGlobalGitConfig("user.name")) {
1428
1619
  this.spawn(["config", "user.name", FALLBACK_USER_NAME]);
1429
1620
  }
@@ -1448,8 +1639,8 @@ var SidecarGit = class {
1448
1639
  */
1449
1640
  async commit(slug, message, opts) {
1450
1641
  if (opts?.skip) return;
1451
- const slugDir = path4.join(this.rootDir, slug);
1452
- if (fs11.existsSync(slugDir)) {
1642
+ const slugDir = path5.join(this.rootDir, slug);
1643
+ if (fs10.existsSync(slugDir)) {
1453
1644
  this.spawn(["add", "--", slug]);
1454
1645
  } else {
1455
1646
  this.spawn(["add", "-A"]);
@@ -1507,181 +1698,30 @@ ${result.stderr}`;
1507
1698
  return result.exitCode === 0;
1508
1699
  }
1509
1700
  };
1510
- var PinPolicySchema = z2.enum(PIN_POLICIES);
1511
- var LibraryInstanceRefSchema = z2.object({
1512
- id: z2.string().uuid(),
1513
- alias: z2.string().optional()
1514
- });
1515
- var LibrarySectionSchema = z2.object({
1516
- pin_policy: PinPolicySchema.default("minor-track"),
1517
- instances: z2.array(LibraryInstanceRefSchema).default([])
1518
- });
1519
- var PresetAppliedItemSchema = z2.object({
1520
- instance_id: z2.string().uuid(),
1521
- def_ref: z2.string()
1522
- });
1523
- var PresetAppliedSchema = z2.object({
1524
- ref: z2.string(),
1525
- applied_at: z2.string(),
1526
- items: z2.array(PresetAppliedItemSchema).default([])
1527
- });
1528
- var WorkspaceConfigV2Schema = z2.object({
1529
- version: z2.literal(2),
1530
- library: LibrarySectionSchema.optional(),
1531
- presets_applied: z2.array(PresetAppliedSchema).optional()
1532
- });
1533
- var LockAssignmentSchema = z2.object({
1534
- assignment_id: z2.string().uuid(),
1535
- instance_id: z2.string().uuid(),
1536
- def_ref: z2.string(),
1537
- resolved_version: z2.string(),
1538
- resolved_sha256: z2.string().optional(),
1539
- pin_policy: PinPolicySchema
1540
- });
1541
- var RequiresEdgeSchema = z2.object({
1542
- from: z2.string(),
1543
- to: z2.string(),
1544
- kind: z2.string().optional()
1545
- });
1546
- var PresetExpandedSchema = z2.object({
1547
- preset_ref: z2.string(),
1548
- instance_ids: z2.array(z2.string().uuid())
1549
- });
1550
- var LockFileSchema = z2.object({
1551
- version: z2.literal(2),
1552
- generated_at: z2.string(),
1553
- assignments: z2.array(LockAssignmentSchema).default([]),
1554
- requires_graph: z2.array(RequiresEdgeSchema).default([]),
1555
- presets_expanded: z2.array(PresetExpandedSchema).default([])
1556
- });
1557
- function parseWorkspaceV2(raw) {
1558
- if (raw.version !== 2) return null;
1559
- return WorkspaceConfigV2Schema.parse(raw);
1560
- }
1561
- function loadWorkspaceV2(dir, filename = "skaile.yaml") {
1562
- const filePath = path4.join(dir, filename);
1563
- if (!fs11.existsSync(filePath)) return null;
1564
- const content = fs11.readFileSync(filePath, "utf-8");
1701
+ function migrateWorkspaceConfig(dir, opts) {
1702
+ const filename = opts?.filename ?? "skaile.yaml";
1703
+ const filePath = path5.join(dir, filename);
1704
+ if (!fs10.existsSync(filePath)) {
1705
+ return { migrated: false, version: 1, warnings: ["No skaile.yaml found"] };
1706
+ }
1707
+ const content = fs10.readFileSync(filePath, "utf-8");
1565
1708
  const raw = parse(content);
1566
- if (!raw || typeof raw !== "object") return null;
1567
- return parseWorkspaceV2(raw);
1568
- }
1569
- function saveWorkspaceV2(dir, v2, filename = "skaile.yaml") {
1570
- const filePath = path4.join(dir, filename);
1571
- let existing = {};
1572
- if (fs11.existsSync(filePath)) {
1573
- const content = fs11.readFileSync(filePath, "utf-8");
1574
- existing = parse(content) ?? {};
1709
+ if (!raw || typeof raw !== "object") {
1710
+ return { migrated: false, version: 1, warnings: ["Invalid YAML content"] };
1575
1711
  }
1576
- existing.version = 2;
1577
- if (v2.library !== void 0) existing.library = v2.library;
1578
- if (v2.presets_applied !== void 0) existing.presets_applied = v2.presets_applied;
1579
- if (!fs11.existsSync(dir)) {
1580
- fs11.mkdirSync(dir, { recursive: true });
1712
+ if (raw.version === 2) {
1713
+ return { migrated: false, version: 2, warnings: [] };
1581
1714
  }
1582
- fs11.writeFileSync(filePath, stringify(existing, { indent: 2 }), "utf-8");
1583
- }
1584
- var LOCK_FILENAME = "skaile.lock.yaml";
1585
- function loadLockFile(dir) {
1586
- const filePath = path4.join(dir, LOCK_FILENAME);
1587
- if (!fs11.existsSync(filePath)) return null;
1588
- const content = fs11.readFileSync(filePath, "utf-8");
1589
- const raw = parse(content);
1590
- if (raw !== null && !("assignments" in raw) && "subscriptions" in raw) {
1591
- const legacySubs = raw["subscriptions"];
1592
- raw["assignments"] = Array.isArray(legacySubs) ? legacySubs.map((s) => {
1593
- if (s !== null && typeof s === "object" && "subscription_id" in s && !("assignment_id" in s)) {
1594
- const { subscription_id, ...rest } = s;
1595
- return { assignment_id: subscription_id, ...rest };
1596
- }
1597
- return s;
1598
- }) : [];
1599
- delete raw["subscriptions"];
1715
+ const warnings = [];
1716
+ if (raw.dependencies && Array.isArray(raw.dependencies)) {
1717
+ warnings.push(
1718
+ "v1 'dependencies' field found. In v2, use 'library.instances' to reference configured asset instances. Manual migration recommended."
1719
+ );
1600
1720
  }
1601
- return LockFileSchema.parse(raw);
1602
- }
1603
- function saveLockFile(dir, lock) {
1604
- const filePath = path4.join(dir, LOCK_FILENAME);
1605
- if (!fs11.existsSync(dir)) {
1606
- fs11.mkdirSync(dir, { recursive: true });
1607
- }
1608
- fs11.writeFileSync(
1609
- filePath,
1610
- `# Auto-generated by skaile. Do not edit manually.
1611
- ${stringify(lock, { indent: 2 })}`,
1612
- "utf-8"
1613
- );
1614
- }
1615
- function createEmptyLockFile() {
1616
- return {
1617
- version: 2,
1618
- generated_at: (/* @__PURE__ */ new Date()).toISOString(),
1619
- assignments: [],
1620
- requires_graph: [],
1621
- presets_expanded: []
1622
- };
1623
- }
1624
- function detectWorkspaceVersion(dir, filename = "skaile.yaml") {
1625
- const filePath = path4.join(dir, filename);
1626
- if (!fs11.existsSync(filePath)) return null;
1627
- const content = fs11.readFileSync(filePath, "utf-8");
1628
- const raw = parse(content);
1629
- if (!raw || typeof raw !== "object") return null;
1630
- return raw.version === 2 ? 2 : 1;
1631
- }
1632
- var MANIFEST_FILENAME = "skaile.manifest.yaml";
1633
- async function writeManifestIfMissing(input) {
1634
- const manifestPath = path4.join(input.libraryPath, MANIFEST_FILENAME);
1635
- if (fs11.existsSync(manifestPath)) return { wrote: false, manifestPath };
1636
- const now = (/* @__PURE__ */ new Date()).toISOString();
1637
- const yaml = `# Generated by skaile library add on ${now} (structure: ${input.structure})
1638
- # Edit by hand to inline discovered assets; or run \`skaile library refresh-manifest\`.
1639
- publisher: ""
1640
- assets: []
1641
- `;
1642
- fs11.writeFileSync(manifestPath, yaml, "utf-8");
1643
- if (input.ownership !== "owner") {
1644
- const excludePath = path4.join(input.libraryPath, ".git", "info", "exclude");
1645
- if (fs11.existsSync(path4.dirname(excludePath))) {
1646
- const current = fs11.existsSync(excludePath) ? fs11.readFileSync(excludePath, "utf-8") : "";
1647
- if (!current.includes(MANIFEST_FILENAME)) {
1648
- fs11.writeFileSync(
1649
- excludePath,
1650
- (current.endsWith("\n") || current === "" ? current : `${current}
1651
- `) + `# skaile-managed local-overlay manifest
1652
- ${MANIFEST_FILENAME}
1653
- `,
1654
- "utf-8"
1655
- );
1656
- }
1657
- }
1658
- }
1659
- return { wrote: true, manifestPath };
1660
- }
1661
- function migrateWorkspaceConfig(dir, opts) {
1662
- const filename = opts?.filename ?? "skaile.yaml";
1663
- const filePath = path4.join(dir, filename);
1664
- if (!fs11.existsSync(filePath)) {
1665
- return { migrated: false, version: 1, warnings: ["No skaile.yaml found"] };
1666
- }
1667
- const content = fs11.readFileSync(filePath, "utf-8");
1668
- const raw = parse(content);
1669
- if (!raw || typeof raw !== "object") {
1670
- return { migrated: false, version: 1, warnings: ["Invalid YAML content"] };
1671
- }
1672
- if (raw.version === 2) {
1673
- return { migrated: false, version: 2, warnings: [] };
1674
- }
1675
- const warnings = [];
1676
- if (raw.dependencies && Array.isArray(raw.dependencies)) {
1677
- warnings.push(
1678
- "v1 'dependencies' field found. In v2, use 'library.instances' to reference configured asset instances. Manual migration recommended."
1679
- );
1680
- }
1681
- if (raw.ai_resources) {
1682
- warnings.push(
1683
- "v1 'ai_resources' field found. In v2, sources are managed via 'skaile source add'. The field still works but is deprecated."
1684
- );
1721
+ if (raw.ai_resources) {
1722
+ warnings.push(
1723
+ "v1 'ai_resources' field found. In v2, sources are managed via 'skaile source add'. The field still works but is deprecated."
1724
+ );
1685
1725
  }
1686
1726
  if (!opts?.dryRun) {
1687
1727
  const doc = parseDocument(content);
@@ -1696,7 +1736,7 @@ function migrateWorkspaceConfig(dir, opts) {
1696
1736
  } else {
1697
1737
  doc.set("version", 2);
1698
1738
  }
1699
- fs11.writeFileSync(filePath, doc.toString(), "utf-8");
1739
+ fs10.writeFileSync(filePath, doc.toString(), "utf-8");
1700
1740
  }
1701
1741
  return {
1702
1742
  migrated: !opts?.dryRun,
@@ -1714,37 +1754,50 @@ function resolvePin(pin, availableVersions) {
1714
1754
  return { version: parsed[0].raw, match: "latest" };
1715
1755
  }
1716
1756
  if (pin.startsWith("exact:")) {
1717
- const target = pin.slice(6);
1718
- const found = parsed.find((v) => v.raw === target);
1719
- return found ? { version: found.raw, match: "exact" } : null;
1757
+ return resolveExactPin(pin, parsed);
1720
1758
  }
1721
1759
  if (pin.startsWith("^")) {
1722
- const target = parseRangeTarget(pin.slice(1));
1723
- if (!target) return null;
1724
- const match = parsed.find(
1725
- (v) => v.major === target.major && (v.minor > target.minor || v.minor === target.minor && v.patch >= target.patch)
1726
- );
1727
- return match ? { version: match.raw, match: "range" } : null;
1760
+ return resolveCaretPin(pin, parsed);
1728
1761
  }
1729
1762
  if (pin.startsWith("~")) {
1730
- const target = parseRangeTarget(pin.slice(1));
1731
- if (!target) return null;
1732
- const match = parsed.find(
1733
- (v) => v.major === target.major && v.minor === target.minor && v.patch >= target.patch
1734
- );
1735
- return match ? { version: match.raw, match: "range" } : null;
1763
+ return resolveTildePin(pin, parsed);
1736
1764
  }
1737
1765
  if (pin.endsWith(".x")) {
1738
- const base = pin.slice(0, -2);
1739
- const parts = base.split(".");
1740
- const major = Number.parseInt(parts[0], 10);
1741
- const minor = parts.length > 1 ? Number.parseInt(parts[1], 10) : void 0;
1742
- if (Number.isNaN(major)) return null;
1743
- const match = parsed.find(
1744
- (v) => v.major === major && (minor === void 0 || v.minor === minor)
1745
- );
1746
- return match ? { version: match.raw, match: "range" } : null;
1766
+ return resolveWildcardPin(pin, parsed);
1747
1767
  }
1768
+ return resolveBarePin(pin, parsed);
1769
+ }
1770
+ function resolveExactPin(pin, parsed) {
1771
+ const target = pin.slice(6);
1772
+ const found = parsed.find((v) => v.raw === target);
1773
+ return found ? { version: found.raw, match: "exact" } : null;
1774
+ }
1775
+ function resolveCaretPin(pin, parsed) {
1776
+ const target = parseRangeTarget(pin.slice(1));
1777
+ if (!target) return null;
1778
+ const match = parsed.find(
1779
+ (v) => v.major === target.major && (v.minor > target.minor || v.minor === target.minor && v.patch >= target.patch)
1780
+ );
1781
+ return match ? { version: match.raw, match: "range" } : null;
1782
+ }
1783
+ function resolveTildePin(pin, parsed) {
1784
+ const target = parseRangeTarget(pin.slice(1));
1785
+ if (!target) return null;
1786
+ const match = parsed.find(
1787
+ (v) => v.major === target.major && v.minor === target.minor && v.patch >= target.patch
1788
+ );
1789
+ return match ? { version: match.raw, match: "range" } : null;
1790
+ }
1791
+ function resolveWildcardPin(pin, parsed) {
1792
+ const base = pin.slice(0, -2);
1793
+ const parts = base.split(".");
1794
+ const major = Number.parseInt(parts[0], 10);
1795
+ const minor = parts.length > 1 ? Number.parseInt(parts[1], 10) : void 0;
1796
+ if (Number.isNaN(major)) return null;
1797
+ const match = parsed.find((v) => v.major === major && (minor === void 0 || v.minor === minor));
1798
+ return match ? { version: match.raw, match: "range" } : null;
1799
+ }
1800
+ function resolveBarePin(pin, parsed) {
1748
1801
  const bareSemver = parseSemVer(pin);
1749
1802
  if (bareSemver) {
1750
1803
  const found = parsed.find((v) => v.raw === pin);
@@ -1780,1793 +1833,1795 @@ function parseRangeTarget(range) {
1780
1833
  };
1781
1834
  }
1782
1835
 
1783
- // library/src/local/local-catalog-source.ts
1784
- var LocalCatalogSource = class {
1785
- constructor(library, sourceId, rootPath, registry, options) {
1786
- this.library = library;
1787
- this.sourceId = sourceId;
1788
- this.rootPath = rootPath;
1789
- this.registry = registry;
1790
- this.id = `local:${sourceId}`;
1791
- this.sidecarPath = options?.sidecarPath;
1792
- }
1793
- library;
1794
- sourceId;
1795
- rootPath;
1796
- registry;
1797
- id;
1798
- sidecarPath;
1799
- async resolve(ref) {
1800
- const def = await this.library.getAssetDef(ref);
1801
- if (!def || def.libraryId !== this.sourceId) return null;
1802
- return defToCatalogAsset(def);
1803
- }
1804
- async listAssets(filter) {
1805
- const libraryFilter = { libraryId: this.sourceId };
1806
- if (filter?.kind) libraryFilter.kind = filter.kind;
1807
- if (filter?.publisher) libraryFilter.publisher = filter.publisher;
1808
- if (filter?.prefix) libraryFilter.prefix = filter.prefix;
1809
- const defs = await this.library.listAssetDefs(libraryFilter);
1810
- return defs.map(defToCatalogAsset);
1836
+ // library/src/preset/interpolate.ts
1837
+ var InterpolationError = class extends Error {
1838
+ constructor(message) {
1839
+ super(message);
1840
+ this.name = "InterpolationError";
1811
1841
  }
1812
- async listVersions(ref) {
1813
- const parsed = parseAssetRef(ref);
1814
- const defs = await this.library.listAssetDefs({
1815
- libraryId: this.sourceId,
1816
- kind: parsed.kind
1817
- });
1818
- const wantPublisher = parsed.publisher?.replace(/^@/, "");
1819
- return defs.filter(
1820
- (d) => d.name === parsed.name && (wantPublisher === void 0 || d.publisher.replace(/^@/, "") === wantPublisher)
1821
- ).map((d) => d.version);
1842
+ };
1843
+ function interpolate(materialize, baseConfig, context, credentialRefPrefix) {
1844
+ const config = { ...baseConfig ?? {} };
1845
+ const secretsToStore = [];
1846
+ let credentialRef;
1847
+ if (!materialize) {
1848
+ for (const [key, value] of Object.entries(context.inputs)) {
1849
+ config[key] = value;
1850
+ }
1851
+ if (Object.keys(context.secrets).length > 0) {
1852
+ credentialRef = `${credentialRefPrefix}/creds`;
1853
+ for (const [key, value] of Object.entries(context.secrets)) {
1854
+ secretsToStore.push({ ref: `${credentialRef}/${key}`, value });
1855
+ }
1856
+ }
1857
+ return { config, credentialRef, secretsToStore };
1822
1858
  }
1823
- async fetchTarball(_ref, _sha256) {
1824
- throw new Error(
1825
- "LocalCatalogSource does not support tarball delivery. Assets are available on disk."
1826
- );
1859
+ for (const [configPath, template] of Object.entries(materialize)) {
1860
+ const resolved = resolveTemplate(template, context);
1861
+ if (resolved.hasSecret) {
1862
+ if (!credentialRef) {
1863
+ credentialRef = `${credentialRefPrefix}/creds`;
1864
+ }
1865
+ const secretRef = `${credentialRef}/${configPath}`;
1866
+ secretsToStore.push({ ref: secretRef, value: resolved.value });
1867
+ setNestedValue(config, configPath, `secret://${secretRef}`);
1868
+ } else {
1869
+ setNestedValue(config, configPath, resolved.value);
1870
+ }
1827
1871
  }
1828
- async listSources() {
1829
- return [{ id: this.sourceId, type: "local", url: this.rootPath }];
1872
+ return { config, credentialRef, secretsToStore };
1873
+ }
1874
+ async function storeSecrets(router, secretsToStore) {
1875
+ for (const { ref, value } of secretsToStore) {
1876
+ const encoded = new TextEncoder().encode(value);
1877
+ await router.setSecret(ref, encoded);
1830
1878
  }
1831
- /**
1832
- * Sync: discover assets in the source directory and cache them in the library.
1833
- *
1834
- * Runs the discovery pipeline (glob -> parse -> filter -> hash -> requires)
1835
- * and upserts each discovered asset into the LocalIndex cache.
1836
- *
1837
- * @param syncOpts - When `includeDev` is set, assets under the source
1838
- * config's `dev_paths` (e.g. `ai-assets-dev/`) are discovered and cached
1839
- * too. Defaults to excluding them (a normal run).
1840
- * @returns Discovery statistics and any errors encountered.
1841
- */
1842
- async sync(syncOpts) {
1843
- const opts = {
1844
- registry: this.registry,
1845
- sidecarPath: this.sidecarPath,
1846
- includeDev: syncOpts?.includeDev ?? false
1847
- };
1848
- if (!this.sidecarPath) {
1849
- opts.sourceConfig = readManifestSourceConfig(this.rootPath);
1879
+ }
1880
+ function hasInterpolationTokens(template) {
1881
+ return INTERPOLATION_PATTERN.test(template);
1882
+ }
1883
+ function extractReferencedKeys(materialize) {
1884
+ const inputKeys = /* @__PURE__ */ new Set();
1885
+ const secretKeys = /* @__PURE__ */ new Set();
1886
+ for (const template of Object.values(materialize)) {
1887
+ const pattern = new RegExp(INTERPOLATION_PATTERN.source, "g");
1888
+ for (const match of template.matchAll(pattern)) {
1889
+ const namespace = match[1];
1890
+ const key = match[2];
1891
+ if (namespace === "INPUT") inputKeys.add(key);
1892
+ else if (namespace === "SECRET") secretKeys.add(key);
1850
1893
  }
1851
- const result = discoverAssetsInTree(this.rootPath, opts);
1852
- let assetsUpdated = 0;
1853
- const errors = [];
1854
- for (const asset of result.assets) {
1855
- if (asset.kind === "preset") continue;
1856
- try {
1857
- await this.library.upsertAssetDef({
1858
- id: asset.ref,
1859
- kind: asset.kind,
1860
- publisher: asset.publisher,
1861
- name: asset.name,
1862
- version: asset.version,
1863
- sha256: asset.sha256,
1864
- description: typeof asset.manifest.description === "string" ? asset.manifest.description : void 0,
1865
- license: typeof asset.manifest.license === "string" ? asset.manifest.license : void 0,
1866
- category: typeof asset.manifest.category === "string" ? asset.manifest.category : void 0,
1867
- manifest: asset.manifest,
1868
- libraryId: this.sourceId,
1869
- cachedAt: /* @__PURE__ */ new Date(),
1870
- updatedAt: /* @__PURE__ */ new Date()
1871
- });
1872
- assetsUpdated++;
1873
- } catch (err) {
1874
- errors.push(`${asset.ref}: ${err instanceof Error ? err.message : String(err)}`);
1894
+ }
1895
+ return { inputKeys: [...inputKeys], secretKeys: [...secretKeys] };
1896
+ }
1897
+ var INTERPOLATION_PATTERN = /\$\{(INPUT|SECRET):([^}]+)\}/;
1898
+ function resolveTemplate(template, context) {
1899
+ const singleMatch = template.match(/^\$\{(INPUT|SECRET):([^}]+)\}$/);
1900
+ if (singleMatch) {
1901
+ const [, namespace, key] = singleMatch;
1902
+ if (namespace === "SECRET") {
1903
+ if (!(key in context.secrets)) {
1904
+ throw new InterpolationError(`Missing secret key "${key}" in interpolation context`);
1875
1905
  }
1906
+ return { value: context.secrets[key], hasSecret: true };
1876
1907
  }
1877
- for (const err of result.errors) {
1878
- errors.push(`${err.path}: ${err.error}`);
1908
+ if (!(key in context.inputs)) {
1909
+ throw new InterpolationError(`Missing input key "${key}" in interpolation context`);
1879
1910
  }
1880
- return { assetsFound: result.assets.length, assetsUpdated, errors };
1911
+ return { value: context.inputs[key], hasSecret: false };
1881
1912
  }
1882
- };
1883
- function defToCatalogAsset(def) {
1884
- return {
1885
- id: def.id,
1886
- kind: def.kind,
1887
- publisher: def.publisher,
1888
- name: def.name,
1889
- version: def.version,
1890
- sha256: def.sha256,
1891
- description: def.description,
1892
- license: def.license,
1893
- category: def.category,
1894
- manifest: def.manifest
1895
- };
1896
- }
1897
- function defaultCacheDir() {
1898
- return path4.join(os.homedir(), ".skaile", "catalog-cache");
1913
+ const tokens = [...template.matchAll(/\$\{(INPUT|SECRET):([^}]+)\}/g)];
1914
+ const hasSecretToken = tokens.some(([, ns]) => ns === "SECRET");
1915
+ if (hasSecretToken) {
1916
+ throw new InterpolationError(
1917
+ `Template "${template}" mixes \${SECRET:...} with other tokens. SECRET must be the sole token in a template value.`
1918
+ );
1919
+ }
1920
+ const result = template.replace(/\$\{INPUT:([^}]+)\}/g, (_, key) => {
1921
+ if (!(key in context.inputs)) {
1922
+ throw new InterpolationError(`Missing input key "${key}" in interpolation context`);
1923
+ }
1924
+ return String(context.inputs[key] ?? "");
1925
+ });
1926
+ return { value: result, hasSecret: false };
1899
1927
  }
1900
- var INDEX_FILE = "index.json";
1901
- var TARBALL_DIR = "tarballs";
1902
- var CatalogCache = class {
1903
- dir;
1904
- ttlMs;
1905
- nowFn;
1906
- index = null;
1907
- constructor(opts) {
1908
- this.dir = opts.dir ?? defaultCacheDir();
1909
- this.ttlMs = opts.ttlMs;
1910
- this.nowFn = opts.now ?? Date.now;
1911
- this.ensureDir();
1912
- }
1913
- // ── public: resolve cache ───────────────────────────────────────────────
1914
- /**
1915
- * Return a fresh resolve-cache entry for `ref`, or `null` if absent / stale.
1916
- *
1917
- * When `ttlMs === 0` (air-gapped), all entries are considered fresh
1918
- * regardless of `fetchedAt`.
1919
- */
1920
- getResolve(ref) {
1921
- const idx = this.loadIndex();
1922
- const entry = idx.resolve[ref];
1923
- if (!entry) return null;
1924
- if (this.ttlMs === 0) return entry;
1925
- if (this.nowFn() - entry.fetchedAt > this.ttlMs) return null;
1926
- return entry;
1927
- }
1928
- /**
1929
- * Return a stale resolve-cache entry regardless of TTL — for stale-while-revalidate.
1930
- */
1931
- getResolveStale(ref) {
1932
- const idx = this.loadIndex();
1933
- return idx.resolve[ref] ?? null;
1934
- }
1935
- /** Persist a resolve-cache entry (use `null` to negatively cache misses). */
1936
- setResolve(ref, value) {
1937
- const idx = this.loadIndex();
1938
- idx.resolve[ref] = { fetchedAt: this.nowFn(), value };
1939
- this.writeIndex(idx);
1940
- }
1941
- // ── public: list cache ──────────────────────────────────────────────────
1942
- /**
1943
- * Return a fresh list-cache entry for `filter`, or `null` if absent / stale.
1944
- */
1945
- getList(filter) {
1946
- const key = filterKey(filter);
1947
- const idx = this.loadIndex();
1948
- const entry = idx.list[key];
1949
- if (!entry) return null;
1950
- if (this.ttlMs === 0) return entry;
1951
- if (this.nowFn() - entry.fetchedAt > this.ttlMs) return null;
1952
- return entry;
1953
- }
1954
- /** Return a stale list-cache entry regardless of TTL — for stale-while-revalidate. */
1955
- getListStale(filter) {
1956
- const key = filterKey(filter);
1957
- const idx = this.loadIndex();
1958
- return idx.list[key] ?? null;
1959
- }
1960
- /** Persist a list-cache entry. */
1961
- setList(filter, items) {
1962
- const key = filterKey(filter);
1963
- const idx = this.loadIndex();
1964
- idx.list[key] = { fetchedAt: this.nowFn(), items };
1965
- this.writeIndex(idx);
1966
- }
1967
- // ── public: tarball cache ───────────────────────────────────────────────
1968
- /** Absolute path where a tarball with `sha256` would be cached. */
1969
- tarballPath(sha256) {
1970
- return path4.join(this.dir, TARBALL_DIR, `${sha256}.tgz`);
1971
- }
1972
- /** Read a cached tarball from disk if present, otherwise `null`. */
1973
- readTarball(sha256) {
1974
- const p = this.tarballPath(sha256);
1975
- if (!fs11.existsSync(p)) return null;
1976
- return fs11.readFileSync(p);
1928
+ function setNestedValue(obj, path13, value) {
1929
+ const parts = path13.split(".");
1930
+ let current = obj;
1931
+ for (let i = 0; i < parts.length - 1; i++) {
1932
+ const key = parts[i];
1933
+ if (current[key] === void 0 || current[key] === null || typeof current[key] !== "object") {
1934
+ current[key] = {};
1935
+ }
1936
+ current = current[key];
1977
1937
  }
1978
- /** Persist a tarball blob to disk (atomic write). */
1979
- writeTarball(sha256, bytes) {
1980
- const dir = path4.join(this.dir, TARBALL_DIR);
1981
- if (!fs11.existsSync(dir)) fs11.mkdirSync(dir, { recursive: true });
1982
- const finalPath = this.tarballPath(sha256);
1983
- const tmpPath = `${finalPath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
1984
- fs11.writeFileSync(tmpPath, bytes);
1985
- fs11.renameSync(tmpPath, finalPath);
1938
+ current[parts[parts.length - 1]] = value;
1939
+ }
1940
+ function parsePreset(data) {
1941
+ const zodResult = PresetManifestSchema.safeParse(data);
1942
+ if (!zodResult.success) {
1943
+ return {
1944
+ ok: false,
1945
+ errors: zodResult.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
1946
+ };
1986
1947
  }
1987
- // ── public: invalidation ────────────────────────────────────────────────
1988
- /**
1989
- * Wipe all cached metadata (resolve + list). Tarballs are content-addressed
1990
- * and remain valid — they are not invalidated.
1991
- *
1992
- * Called by `skaile update`.
1993
- */
1994
- invalidateAll() {
1995
- const idx = { version: 1, resolve: {}, list: {} };
1996
- this.index = idx;
1997
- this.writeIndex(idx);
1948
+ const preset = zodResult.data;
1949
+ const errors = [];
1950
+ const ids = validateUniqueIds(preset, errors);
1951
+ validateCrossItemRefs(preset, ids, errors);
1952
+ validatePlaceholderItems(preset, errors);
1953
+ if (errors.length > 0) {
1954
+ return { ok: false, preset, errors };
1998
1955
  }
1999
- /**
2000
- * Wipe all cached metadata **and** delete tarball blobs. Used for full reset.
2001
- */
2002
- invalidateEverything() {
2003
- this.invalidateAll();
2004
- const tarballDir = path4.join(this.dir, TARBALL_DIR);
2005
- if (fs11.existsSync(tarballDir)) {
2006
- fs11.rmSync(tarballDir, { recursive: true, force: true });
1956
+ return { ok: true, preset, errors: [] };
1957
+ }
1958
+ function validateUniqueIds(preset, errors) {
1959
+ const ids = /* @__PURE__ */ new Set();
1960
+ const refCounts = /* @__PURE__ */ new Map();
1961
+ for (const item of preset.items) {
1962
+ if (item.id) {
1963
+ if (ids.has(item.id)) {
1964
+ errors.push(`Duplicate item id: "${item.id}"`);
1965
+ }
1966
+ ids.add(item.id);
1967
+ } else {
1968
+ refCounts.set(item.ref, (refCounts.get(item.ref) ?? 0) + 1);
2007
1969
  }
2008
1970
  }
2009
- /** Return whether `ttlMs` is zero (air-gapped mode). */
2010
- get airGapped() {
2011
- return this.ttlMs === 0;
2012
- }
2013
- // ── private ─────────────────────────────────────────────────────────────
2014
- ensureDir() {
2015
- if (!fs11.existsSync(this.dir)) fs11.mkdirSync(this.dir, { recursive: true });
2016
- const tarballDir = path4.join(this.dir, TARBALL_DIR);
2017
- if (!fs11.existsSync(tarballDir)) fs11.mkdirSync(tarballDir, { recursive: true });
2018
- }
2019
- indexPath() {
2020
- return path4.join(this.dir, INDEX_FILE);
1971
+ for (const [ref, count] of refCounts) {
1972
+ if (count > 1) {
1973
+ errors.push(
1974
+ `${count} items share ref "${ref}" without distinct "id" fields \u2014 upgrade matching will be ambiguous`
1975
+ );
1976
+ }
2021
1977
  }
2022
- loadIndex() {
2023
- if (this.index) return this.index;
2024
- const p = this.indexPath();
2025
- if (!fs11.existsSync(p)) {
2026
- this.index = { version: 1, resolve: {}, list: {} };
2027
- return this.index;
1978
+ return ids;
1979
+ }
1980
+ function validateCrossItemRefs(preset, ids, errors) {
1981
+ for (const item of preset.items) {
1982
+ if (item.composes) {
1983
+ for (const compose of item.composes) {
1984
+ if (compose.ref.startsWith("#")) {
1985
+ const targetId = compose.ref.slice(1);
1986
+ if (!ids.has(targetId)) {
1987
+ errors.push(
1988
+ `Cross-item ref "${compose.ref}" in item "${item.id ?? item.ref}" references undeclared id "${targetId}"`
1989
+ );
1990
+ }
1991
+ }
1992
+ }
2028
1993
  }
2029
- try {
2030
- const raw = JSON.parse(fs11.readFileSync(p, "utf-8"));
2031
- if (raw && typeof raw === "object" && raw.version === 1) {
2032
- this.index = {
2033
- version: 1,
2034
- resolve: raw.resolve ?? {},
2035
- list: raw.list ?? {}
2036
- };
2037
- return this.index;
1994
+ }
1995
+ }
1996
+ function validatePlaceholderItems(preset, errors) {
1997
+ for (const item of preset.items) {
1998
+ if (item.placeholders) {
1999
+ const keys = /* @__PURE__ */ new Set();
2000
+ for (const ph of item.placeholders) {
2001
+ if (keys.has(ph.key)) {
2002
+ errors.push(`Duplicate placeholder key "${ph.key}" in item "${item.id ?? item.ref}"`);
2003
+ }
2004
+ keys.add(ph.key);
2005
+ if (ph.type === "enum" && (!ph.values || ph.values.length === 0)) {
2006
+ errors.push(`Placeholder "${ph.key}" has type "enum" but no values defined`);
2007
+ }
2038
2008
  }
2039
- } catch {
2040
2009
  }
2041
- this.index = { version: 1, resolve: {}, list: {} };
2042
- return this.index;
2043
2010
  }
2044
- writeIndex(idx) {
2045
- this.index = idx;
2046
- const p = this.indexPath();
2047
- const tmp = `${p}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
2048
- fs11.writeFileSync(tmp, JSON.stringify(idx, null, 2), "utf-8");
2049
- fs11.renameSync(tmp, p);
2011
+ }
2012
+ function parsePresetYaml(yamlContent) {
2013
+ let data;
2014
+ try {
2015
+ data = parse(yamlContent);
2016
+ } catch (err) {
2017
+ return {
2018
+ ok: false,
2019
+ errors: [`YAML parse error: ${err instanceof Error ? err.message : String(err)}`]
2020
+ };
2050
2021
  }
2051
- };
2052
- function filterKey(filter) {
2053
- if (!filter) return "__all__";
2054
- const keys = Object.keys(filter).sort();
2055
- const norm = {};
2056
- for (const k of keys) {
2057
- const v = filter[k];
2058
- if (v !== void 0) norm[k] = v;
2022
+ return parsePreset(data);
2023
+ }
2024
+ function detectNestedPresetRefs(preset, knownPresetRefs) {
2025
+ const nested = [];
2026
+ for (const item of preset.items) {
2027
+ if (item.ref.startsWith("library://") || item.ref.startsWith("#")) continue;
2028
+ if (knownPresetRefs?.has(item.ref)) {
2029
+ nested.push(item.ref);
2030
+ }
2059
2031
  }
2060
- return Object.keys(norm).length === 0 ? "__all__" : JSON.stringify(norm);
2032
+ return nested;
2061
2033
  }
2062
- function sha256Hex(bytes) {
2063
- const h = crypto.createHash("sha256");
2064
- h.update(bytes);
2065
- return h.digest("hex");
2034
+ function validateNestingDepth(presetRef, currentDepth) {
2035
+ const MAX_DEPTH = 3;
2036
+ if (currentDepth >= MAX_DEPTH) {
2037
+ return `Preset "${presetRef}" exceeds maximum nesting depth (${MAX_DEPTH} levels allowed, depth ${currentDepth} reached)`;
2038
+ }
2039
+ return void 0;
2066
2040
  }
2067
2041
 
2068
- // library/src/remote/remote-catalog-source.ts
2069
- var TarballHashMismatchError = class extends Error {
2070
- constructor(expected, actual) {
2071
- super(`SHA256 mismatch: expected ${expected}, got ${actual}`);
2072
- this.expected = expected;
2073
- this.actual = actual;
2074
- this.name = "TarballHashMismatchError";
2075
- }
2076
- expected;
2077
- actual;
2078
- };
2079
- var OfflineError = class extends Error {
2080
- constructor(message) {
2081
- super(`${message} (air-gapped mode; run \`skaile update\` to refresh the cache)`);
2082
- this.name = "OfflineError";
2042
+ // library/src/preset/placeholders.ts
2043
+ function validatePlaceholder(placeholder, value) {
2044
+ const { key, type = "text", required } = placeholder;
2045
+ if (value === void 0 || value === null || value === "") {
2046
+ if (placeholder.default !== void 0) {
2047
+ return { valid: true, errors: [], value: placeholder.default };
2048
+ }
2049
+ if (required === true || required === void 0) {
2050
+ return { valid: false, errors: [`Placeholder "${key}" is required`] };
2051
+ }
2052
+ return { valid: true, errors: [], value: void 0 };
2083
2053
  }
2084
- };
2085
- var CatalogHttpError = class extends Error {
2086
- constructor(message, status, url) {
2087
- super(message);
2088
- this.status = status;
2089
- this.url = url;
2090
- this.name = "CatalogHttpError";
2091
- }
2092
- status;
2093
- url;
2094
- };
2095
- var DEFAULT_TTL_MS = 864e5;
2096
- var RemoteCatalogSource = class {
2097
- id;
2098
- baseUrl;
2099
- cache;
2100
- cacheTtlMs;
2101
- httpClient;
2102
- silentBackgroundErrors;
2103
- /** Tracks in-flight background revalidations so callers can `await` them in tests. */
2104
- inFlight = /* @__PURE__ */ new Map();
2105
- constructor(opts) {
2106
- this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
2107
- this.id = `remote:${this.baseUrl}`;
2108
- this.cacheTtlMs = opts.cacheTtlMs ?? DEFAULT_TTL_MS;
2109
- this.httpClient = opts.httpClient ?? defaultFetch;
2110
- this.silentBackgroundErrors = opts.silentBackgroundErrors ?? false;
2111
- this.cache = opts.cache ?? new CatalogCache({
2112
- dir: opts.cacheDir,
2113
- ttlMs: this.cacheTtlMs,
2114
- now: opts.now
2115
- });
2054
+ switch (type) {
2055
+ case "text":
2056
+ return validateText(key, value);
2057
+ case "multiline":
2058
+ return validateMultiline(key, value);
2059
+ case "secret":
2060
+ return validateSecret(key, value);
2061
+ case "number":
2062
+ return validateNumber(key, value);
2063
+ case "boolean":
2064
+ return validateBoolean(key, value);
2065
+ case "enum":
2066
+ return validateEnum(key, value, placeholder.values);
2067
+ case "oauth":
2068
+ return validateOauth(key, value);
2069
+ case "instance-picker":
2070
+ return validateInstancePicker(key, value);
2116
2071
  }
2117
- // ── ICatalogSource ──────────────────────────────────────────────────────
2118
- /**
2119
- * Resolve a single asset by canonical ref (`<publisher>/<name>@<version>`).
2120
- *
2121
- * Cache: hit returns immediately. Miss fetches `/trpc/catalog.get`. Stale
2122
- * entry serves the cached value and triggers a background refresh.
2123
- *
2124
- * Air-gapped: throws {@link OfflineError} on cache miss.
2125
- */
2126
- async resolve(ref) {
2127
- const fresh = this.cache.getResolve(ref);
2128
- if (fresh) return fresh.value;
2129
- if (this.cache.airGapped) {
2130
- const stale2 = this.cache.getResolveStale(ref);
2131
- if (stale2) return stale2.value;
2132
- throw new OfflineError(`Asset '${ref}' not in cache`);
2133
- }
2134
- const stale = this.cache.getResolveStale(ref);
2135
- if (stale) {
2136
- this.scheduleBackground(`resolve:${ref}`, () => this.fetchAndStoreResolve(ref));
2137
- return stale.value;
2072
+ }
2073
+ function validateAllPlaceholders(placeholders, inputs) {
2074
+ const values = {};
2075
+ const errors = [];
2076
+ for (const ph of placeholders) {
2077
+ const result = validatePlaceholder(ph, inputs[ph.key]);
2078
+ if (!result.valid) {
2079
+ errors.push(...result.errors);
2080
+ } else if (result.value !== void 0) {
2081
+ values[ph.key] = result.value;
2138
2082
  }
2139
- return this.fetchAndStoreResolve(ref);
2140
2083
  }
2141
- /**
2142
- * List assets matching `filter`.
2143
- *
2144
- * Cache: hit returns immediately. Miss fetches `/trpc/catalog.list`. Stale
2145
- * entry serves the cached array and triggers a background refresh.
2146
- *
2147
- * Air-gapped: throws {@link OfflineError} on cache miss.
2148
- */
2149
- async listAssets(filter) {
2150
- const fresh = this.cache.getList(filter);
2151
- if (fresh) return fresh.items;
2152
- if (this.cache.airGapped) {
2153
- const stale2 = this.cache.getListStale(filter);
2154
- if (stale2) return stale2.items;
2155
- throw new OfflineError("Catalog list not in cache");
2156
- }
2157
- const stale = this.cache.getListStale(filter);
2158
- if (stale) {
2159
- const filterKey2 = JSON.stringify(filter ?? null);
2160
- this.scheduleBackground(`list:${filterKey2}`, () => this.fetchAndStoreList(filter));
2161
- return stale.items;
2162
- }
2163
- return this.fetchAndStoreList(filter);
2084
+ return { valid: errors.length === 0, values, errors };
2085
+ }
2086
+ async function queryInstancePickerChoices(library, options) {
2087
+ const filter = {};
2088
+ if (options?.kind) {
2089
+ filter.kind = options.kind;
2164
2090
  }
2165
- /**
2166
- * Fetch the content-addressed tarball for an asset. The bytes are downloaded
2167
- * via the REST endpoint `GET /tarball/:sha256` (which 307-redirects to the
2168
- * storage tier), then verified against `sha256` on the client side.
2169
- *
2170
- * Cache: tarballs are content-addressed and cached forever once written.
2171
- *
2172
- * Air-gapped: throws {@link OfflineError} on cache miss.
2173
- *
2174
- * @throws {@link TarballHashMismatchError} when the downloaded SHA256 does not match.
2175
- */
2176
- async fetchTarball(_ref, sha256) {
2177
- const cached = this.cache.readTarball(sha256);
2178
- if (cached) {
2179
- const actual2 = sha256Hex(cached);
2180
- if (actual2 !== sha256) {
2181
- throw new TarballHashMismatchError(sha256, actual2);
2182
- }
2183
- return cached;
2184
- }
2185
- if (this.cache.airGapped) {
2186
- throw new OfflineError(`Tarball '${sha256}' not in cache`);
2187
- }
2188
- const url = `${this.baseUrl}/tarball/${encodeURIComponent(sha256)}`;
2189
- const res = await this.httpClient(url, { method: "GET", redirect: "follow" });
2190
- if (!res.ok) {
2191
- throw new CatalogHttpError(
2192
- `Failed to fetch tarball ${sha256}: ${res.status} ${res.statusText}`,
2193
- res.status,
2194
- url
2195
- );
2196
- }
2197
- const buf = new Uint8Array(await res.arrayBuffer());
2198
- const actual = sha256Hex(buf);
2199
- if (actual !== sha256) {
2200
- throw new TarballHashMismatchError(sha256, actual);
2201
- }
2202
- this.cache.writeTarball(sha256, buf);
2203
- return buf;
2091
+ if (options?.defRefPrefix) {
2092
+ filter.defRef = options.defRefPrefix;
2204
2093
  }
2205
- /**
2206
- * Enumerate available versions for a version-less ref. The Catalog has no
2207
- * dedicated version-list route, so this derives versions from `listAssets`
2208
- * (filtered to the publisher) and keeps only the matching kind+name. Reuses
2209
- * the cached list path, so repeated calls are cheap.
2210
- */
2211
- async listVersions(ref) {
2212
- const parsed = parseAssetRef(ref);
2213
- const wantPublisher = parsed.publisher?.replace(/^@/, "");
2214
- const assets = await this.listAssets(wantPublisher ? { publisher: wantPublisher } : void 0);
2215
- return assets.filter(
2216
- (a) => a.kind === parsed.kind && a.name === parsed.name && (wantPublisher === void 0 || a.publisher.replace(/^@/, "") === wantPublisher)
2217
- ).map((a) => a.version);
2094
+ const instances2 = await library.listInstances(filter);
2095
+ return instances2.map((inst) => ({
2096
+ instanceId: inst.id,
2097
+ defRef: inst.defRef,
2098
+ label: `${inst.defRef} (${inst.id.slice(0, 8)})`
2099
+ }));
2100
+ }
2101
+ function getDefaultValue(placeholder) {
2102
+ return placeholder.default;
2103
+ }
2104
+ function isSecretPlaceholder(placeholder) {
2105
+ return placeholder.type === "secret" || placeholder.type === "oauth";
2106
+ }
2107
+ function validateText(key, value) {
2108
+ if (typeof value !== "string") {
2109
+ return { valid: false, errors: [`Placeholder "${key}" must be a string`] };
2218
2110
  }
2219
- /**
2220
- * Single synthetic Source pointing at the configured Catalog URL. Phase 2
2221
- * is single-Source per spec; multi-source federation is Phase 4.8.
2222
- */
2223
- async listSources() {
2224
- return [{ id: this.id, type: "remote-catalog", url: this.baseUrl }];
2111
+ return { valid: true, errors: [], value };
2112
+ }
2113
+ function validateMultiline(key, value) {
2114
+ if (typeof value !== "string") {
2115
+ return { valid: false, errors: [`Placeholder "${key}" must be a string`] };
2225
2116
  }
2226
- // ── extension API ───────────────────────────────────────────────────────
2227
- /**
2228
- * Force a metadata-cache refresh. Wipes all resolve/list entries and
2229
- * re-fetches `listAssets()` (no filter) to pre-warm the canonical query.
2230
- *
2231
- * Driven by `skaile update`. Tarballs (content-addressed) are not touched
2232
- * they remain valid across refreshes.
2233
- *
2234
- * In air-gapped mode this method **does** make a network call (bypassing
2235
- * the TTL=0 gate); it is the only way to repopulate the cache.
2236
- */
2237
- async refresh() {
2238
- this.cache.invalidateAll();
2239
- const items = await this.fetchListNetwork(void 0);
2240
- this.cache.setList(void 0, items);
2241
- for (const item of items) {
2242
- this.cache.setResolve(item.id, item);
2117
+ return { valid: true, errors: [], value };
2118
+ }
2119
+ function validateSecret(key, value) {
2120
+ if (typeof value !== "string") {
2121
+ return { valid: false, errors: [`Placeholder "${key}" must be a string`] };
2122
+ }
2123
+ if (value.length === 0) {
2124
+ return { valid: false, errors: [`Placeholder "${key}" (secret) cannot be empty`] };
2125
+ }
2126
+ return { valid: true, errors: [], value };
2127
+ }
2128
+ function validateNumber(key, value) {
2129
+ if (typeof value === "number" && !Number.isNaN(value)) {
2130
+ return { valid: true, errors: [], value };
2131
+ }
2132
+ if (typeof value === "string") {
2133
+ const parsed = Number(value);
2134
+ if (!Number.isNaN(parsed)) {
2135
+ return { valid: true, errors: [], value: parsed };
2243
2136
  }
2244
- return { assetsCached: items.length };
2245
2137
  }
2246
- /**
2247
- * Fetch the pointer-only install manifest for an asset ref.
2248
- *
2249
- * The install manifest is the Pointer Triple: the immutable upstream
2250
- * `source.url` + `source.commitSha`, the per-file `files[].sha256` list, a
2251
- * composite `sha256` rollup, plus `kind` and `tier`. Callers fetch the asset
2252
- * bytes directly from the upstream repo at that commit and verify each file
2253
- * against the recorded hashes — the Catalog itself never serves asset bytes.
2254
- *
2255
- * Wire surface: `GET /trpc/catalog.getInstallManifest?input=<superjson({ ref })>`.
2256
- *
2257
- * This is a `RemoteCatalogSource`-only method — it is not part of the
2258
- * `ICatalogSource` contract. `LocalCatalogSource` has the asset bytes on disk
2259
- * and has no pointer-triple concept, so adding it to the shared interface
2260
- * would force a meaningless implementation there.
2261
- *
2262
- * Not cached: the install path is a one-shot read driven by `skaile install`.
2263
- *
2264
- * @throws {@link CatalogHttpError} on a non-2xx response or malformed envelope.
2265
- */
2266
- async getInstallManifest(ref) {
2267
- const url = trpcGetUrl(this.baseUrl, "catalog.getInstallManifest", { ref });
2268
- const res = await this.httpClient(url, {
2269
- method: "GET",
2270
- headers: { Accept: "application/json" }
2271
- });
2272
- if (!res.ok) {
2273
- throw new CatalogHttpError(
2274
- `catalog.getInstallManifest ${ref} failed: ${res.status} ${res.statusText}`,
2275
- res.status,
2276
- url
2277
- );
2138
+ return { valid: false, errors: [`Placeholder "${key}" must be a valid number`] };
2139
+ }
2140
+ function validateBoolean(key, value) {
2141
+ if (typeof value === "boolean") {
2142
+ return { valid: true, errors: [], value };
2143
+ }
2144
+ if (typeof value === "string") {
2145
+ const lower = value.toLowerCase();
2146
+ if (lower === "true" || lower === "1" || lower === "yes") {
2147
+ return { valid: true, errors: [], value: true };
2278
2148
  }
2279
- const data = await parseTrpcGet(res);
2280
- if (data === null || data === void 0 || typeof data !== "object") {
2281
- throw new CatalogHttpError(
2282
- `catalog.getInstallManifest ${ref} returned a non-object payload (got ${typeof data})`,
2283
- 200,
2284
- url
2285
- );
2149
+ if (lower === "false" || lower === "0" || lower === "no") {
2150
+ return { valid: true, errors: [], value: false };
2286
2151
  }
2287
- return data;
2288
2152
  }
2289
- /**
2290
- * Cheap probe used during cross-check when both a source-side and a store-side
2291
- * candidate at the same version need to be verified for sha256 equality.
2292
- * Returns only the composite sha256 — no source URL, no file list.
2293
- *
2294
- * Wire: `GET /trpc/catalog.getCanonicalDigest?input=<superjson({ ref })>`.
2295
- *
2296
- * Not cached: one-shot read during install resolution.
2297
- *
2298
- * @param ref - Canonical asset ref `<publisher>/<kind>:<name>@<version>`.
2299
- * @returns `{ sha256 }` on a 200, or `null` on a 404.
2300
- * @throws {@link CatalogHttpError} on a non-2xx/non-404 response or malformed payload.
2301
- * @docLink packages/library/api-reference#remote-catalog-source
2302
- */
2303
- async getCanonicalDigest(ref) {
2304
- const url = trpcGetUrl(this.baseUrl, "catalog.getCanonicalDigest", { ref });
2305
- const res = await this.httpClient(url, {
2306
- method: "GET",
2307
- headers: { Accept: "application/json" }
2308
- });
2309
- if (res.status === 404) return null;
2310
- if (!res.ok) {
2311
- throw new CatalogHttpError(
2312
- `catalog.getCanonicalDigest ${ref} failed: ${res.status} ${res.statusText}`,
2313
- res.status,
2314
- url
2315
- );
2316
- }
2317
- const data = await parseTrpcGet(res);
2318
- if (!data || typeof data !== "object") {
2319
- throw new CatalogHttpError(
2320
- `catalog.getCanonicalDigest ${ref} returned a non-object payload`,
2321
- 200,
2322
- url
2323
- );
2153
+ return { valid: false, errors: [`Placeholder "${key}" must be a boolean`] };
2154
+ }
2155
+ function validateEnum(key, value, allowedValues) {
2156
+ if (!allowedValues || allowedValues.length === 0) {
2157
+ return { valid: false, errors: [`Placeholder "${key}" (enum) has no allowed values defined`] };
2158
+ }
2159
+ const stringValue = String(value);
2160
+ const matches = allowedValues.some((v) => String(v) === stringValue);
2161
+ if (!matches) {
2162
+ return {
2163
+ valid: false,
2164
+ errors: [
2165
+ `Placeholder "${key}" value "${stringValue}" is not in allowed values: [${allowedValues.join(", ")}]`
2166
+ ]
2167
+ };
2168
+ }
2169
+ const matched = allowedValues.find((v) => String(v) === stringValue);
2170
+ return { valid: true, errors: [], value: matched };
2171
+ }
2172
+ function validateOauth(key, value) {
2173
+ if (typeof value !== "string") {
2174
+ return { valid: false, errors: [`Placeholder "${key}" (oauth) must be a string`] };
2175
+ }
2176
+ if (value.length === 0) {
2177
+ return { valid: false, errors: [`Placeholder "${key}" (oauth) cannot be empty`] };
2178
+ }
2179
+ return { valid: true, errors: [], value };
2180
+ }
2181
+ function validateInstancePicker(key, value) {
2182
+ if (typeof value !== "string") {
2183
+ return {
2184
+ valid: false,
2185
+ errors: [`Placeholder "${key}" (instance-picker) must be a string UUID`]
2186
+ };
2187
+ }
2188
+ const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2189
+ if (!uuidPattern.test(value)) {
2190
+ return {
2191
+ valid: false,
2192
+ errors: [`Placeholder "${key}" (instance-picker) must be a valid UUID`]
2193
+ };
2194
+ }
2195
+ return { valid: true, errors: [], value };
2196
+ }
2197
+
2198
+ // library/src/preset/resolve-item.ts
2199
+ function classifyRef(ref) {
2200
+ if (ref.startsWith("library://")) return "library-instance";
2201
+ if (ref.startsWith("#")) return "cross-item";
2202
+ return "asset";
2203
+ }
2204
+ function extractLibraryInstanceId(ref) {
2205
+ const match = ref.match(/^library:\/\/[^/]+\/instances\/([a-f0-9-]+)$/i);
2206
+ return match ? match[1] : null;
2207
+ }
2208
+ async function resolveItem(item, library, knownPresetRefs) {
2209
+ const ref = item.ref;
2210
+ if (ref.startsWith("#")) {
2211
+ return { kind: "cross-item", item, targetId: ref.slice(1) };
2212
+ }
2213
+ if (ref.startsWith("library://")) {
2214
+ const instanceId = extractLibraryInstanceId(ref);
2215
+ if (!instanceId) {
2216
+ return { kind: "library-instance", item, instanceId: "", instance: null };
2324
2217
  }
2325
- const d = data;
2326
- if (typeof d.sha256 !== "string") {
2327
- throw new CatalogHttpError(
2328
- `catalog.getCanonicalDigest ${ref} missing sha256 string`,
2329
- 200,
2330
- url
2331
- );
2218
+ const instance = await library.getInstance(instanceId);
2219
+ return { kind: "library-instance", item, instanceId, instance };
2220
+ }
2221
+ if (knownPresetRefs?.has(ref)) {
2222
+ return { kind: "nested-preset", item, presetRef: ref };
2223
+ }
2224
+ const assetDef = await library.getAssetDef(ref);
2225
+ return { kind: "asset", item, defRef: ref, assetDef };
2226
+ }
2227
+ async function resolveAllItems(items, library, knownPresetRefs) {
2228
+ return Promise.all(items.map((item) => resolveItem(item, library, knownPresetRefs)));
2229
+ }
2230
+
2231
+ // library/src/preset/apply.ts
2232
+ async function applyPreset(presetRef, manifest, inputs, opts) {
2233
+ const depth = opts._depth ?? 0;
2234
+ const pinPolicy = opts.pinPolicy ?? "minor-track";
2235
+ const depthError = validateNestingDepth(presetRef, depth);
2236
+ if (depthError) {
2237
+ return { instances: [], assignments: [], errors: [depthError] };
2238
+ }
2239
+ const result = { instances: [], assignments: [], errors: [] };
2240
+ const itemResults = /* @__PURE__ */ new Map();
2241
+ for (const item of manifest.items) {
2242
+ const itemResult = await applyItemImpl(
2243
+ presetRef,
2244
+ item,
2245
+ inputs,
2246
+ opts,
2247
+ depth,
2248
+ pinPolicy,
2249
+ itemResults
2250
+ );
2251
+ result.errors.push(...itemResult.errors);
2252
+ result.instances.push(...itemResult.instances);
2253
+ if (item.id && itemResult.instances.length > 0) {
2254
+ itemResults.set(item.id, itemResult.instances[0]);
2332
2255
  }
2333
- return { sha256: d.sha256 };
2256
+ result.assignments.push(...itemResult.assignments);
2257
+ }
2258
+ return result;
2259
+ }
2260
+ async function applyPresetFromData(presetRef, data, inputs, opts) {
2261
+ const parseResult = parsePreset(data);
2262
+ if (!parseResult.ok || !parseResult.preset) {
2263
+ return {
2264
+ instances: [],
2265
+ assignments: [],
2266
+ errors: parseResult.errors.map((e) => `Parse: ${e}`)
2267
+ };
2334
2268
  }
2335
- /**
2336
- * Wait for any in-flight stale-while-revalidate background fetches to settle.
2337
- * Test-only escape hatch.
2338
- */
2339
- async waitForBackground() {
2340
- const promises = Array.from(this.inFlight.values());
2341
- await Promise.allSettled(promises);
2269
+ return applyPreset(presetRef, parseResult.preset, inputs, opts);
2270
+ }
2271
+ async function applyItemImpl(presetRef, item, inputs, opts, depth, pinPolicy, itemResults) {
2272
+ const { library } = opts;
2273
+ const resolved = await resolveItem(item, library, opts.knownPresetRefs);
2274
+ switch (resolved.kind) {
2275
+ case "cross-item":
2276
+ return applyCrossItem(resolved, itemResults);
2277
+ case "library-instance":
2278
+ return applyLibraryInstance(resolved, opts, pinPolicy);
2279
+ case "nested-preset":
2280
+ return applyNestedPreset(resolved, item, inputs, opts, depth);
2281
+ case "asset":
2282
+ return applyAssetItem(presetRef, resolved, item, inputs, opts, pinPolicy);
2283
+ }
2284
+ }
2285
+ function applyCrossItem(resolved, itemResults) {
2286
+ const target = itemResults.get(resolved.targetId);
2287
+ if (!target) {
2288
+ return {
2289
+ instances: [],
2290
+ assignments: [],
2291
+ errors: [`Cross-item ref "#${resolved.targetId}" not yet resolved (items must be ordered)`]
2292
+ };
2342
2293
  }
2343
- // ── private ─────────────────────────────────────────────────────────────
2344
- scheduleBackground(key, fn) {
2345
- if (this.inFlight.has(key)) return;
2346
- const p = (async () => {
2347
- try {
2348
- await fn();
2349
- } catch (err) {
2350
- if (!this.silentBackgroundErrors) {
2351
- process.stderr.write(
2352
- `[skaile/catalog] Background refresh failed for ${key}: ${err instanceof Error ? err.message : String(err)}
2353
- `
2354
- );
2355
- }
2356
- } finally {
2357
- this.inFlight.delete(key);
2358
- }
2359
- })();
2360
- this.inFlight.set(key, p);
2294
+ return { instances: [target], assignments: [], errors: [] };
2295
+ }
2296
+ async function applyLibraryInstance(resolved, opts, pinPolicy) {
2297
+ if (!resolved.instance) {
2298
+ return {
2299
+ instances: [],
2300
+ assignments: [],
2301
+ errors: [`Library instance "${resolved.instanceId}" not found`]
2302
+ };
2361
2303
  }
2362
- async fetchAndStoreResolve(ref) {
2363
- const value = await this.fetchResolveNetwork(ref);
2364
- this.cache.setResolve(ref, value);
2365
- return value;
2304
+ try {
2305
+ const asgn = await opts.library.assign(opts.workspaceId, resolved.instanceId, pinPolicy);
2306
+ return { instances: [resolved.instance], assignments: [asgn], errors: [] };
2307
+ } catch {
2308
+ return { instances: [resolved.instance], assignments: [], errors: [] };
2366
2309
  }
2367
- async fetchAndStoreList(filter) {
2368
- const items = await this.fetchListNetwork(filter);
2369
- this.cache.setList(filter, items);
2370
- return items;
2310
+ }
2311
+ async function applyNestedPreset(resolved, item, inputs, opts, depth) {
2312
+ if (!opts.loadPreset) {
2313
+ return {
2314
+ instances: [],
2315
+ assignments: [],
2316
+ errors: [`Cannot resolve nested preset "${resolved.presetRef}" \u2014 no loader provided`]
2317
+ };
2371
2318
  }
2372
- /** Raw network call: GET /trpc/catalog.get?input=<superjson> */
2373
- async fetchResolveNetwork(ref) {
2374
- const url = trpcGetUrl(this.baseUrl, "catalog.get", { ref });
2375
- const res = await this.httpClient(url, {
2376
- method: "GET",
2377
- headers: { Accept: "application/json" }
2378
- });
2379
- if (!res.ok) {
2380
- throw new CatalogHttpError(
2381
- `catalog.get ${ref} failed: ${res.status} ${res.statusText}`,
2382
- res.status,
2383
- url
2384
- );
2319
+ const nestedData = await opts.loadPreset(resolved.presetRef);
2320
+ if (!nestedData) {
2321
+ return {
2322
+ instances: [],
2323
+ assignments: [],
2324
+ errors: [`Nested preset "${resolved.presetRef}" not found`]
2325
+ };
2326
+ }
2327
+ const parseResult = parsePreset(nestedData);
2328
+ if (!parseResult.ok || !parseResult.preset) {
2329
+ return { instances: [], assignments: [], errors: parseResult.errors };
2330
+ }
2331
+ const nestedInputs = {
2332
+ ...item.inputs ?? {},
2333
+ ...inputs
2334
+ };
2335
+ const nestedResult = await applyPreset(resolved.presetRef, parseResult.preset, nestedInputs, {
2336
+ ...opts,
2337
+ _depth: depth + 1
2338
+ });
2339
+ return {
2340
+ instances: nestedResult.instances,
2341
+ assignments: nestedResult.assignments,
2342
+ errors: nestedResult.errors
2343
+ };
2344
+ }
2345
+ function validateAssetPlaceholders(item, inputs) {
2346
+ const itemInputs = { ...inputs };
2347
+ let validatedValues = {};
2348
+ const secretValues = {};
2349
+ if (item.placeholders && item.placeholders.length > 0) {
2350
+ const nonSecretPhs = item.placeholders.filter((ph) => !isSecretPlaceholder(ph));
2351
+ const secretPhs = item.placeholders.filter((ph) => isSecretPlaceholder(ph));
2352
+ const validation = validateAllPlaceholders(nonSecretPhs, itemInputs);
2353
+ if (!validation.valid) {
2354
+ return { validatedValues: {}, secretValues: {}, errors: validation.errors };
2355
+ }
2356
+ validatedValues = validation.values;
2357
+ const secretValidation = validateAllPlaceholders(secretPhs, itemInputs);
2358
+ if (!secretValidation.valid) {
2359
+ return { validatedValues: {}, secretValues: {}, errors: secretValidation.errors };
2360
+ }
2361
+ for (const [key, value] of Object.entries(secretValidation.values)) {
2362
+ secretValues[key] = String(value);
2385
2363
  }
2386
- const data = await parseTrpcGet(res);
2387
- if (data === null || data === void 0) return null;
2388
- return normalizeCatalogAsset(data);
2389
2364
  }
2390
- /** Raw network call: GET /trpc/catalog.list?input=<superjson> */
2391
- async fetchListNetwork(filter) {
2392
- const input = filter ? { filter } : {};
2393
- const url = trpcGetUrl(this.baseUrl, "catalog.list", input);
2394
- const res = await this.httpClient(url, {
2395
- method: "GET",
2396
- headers: { Accept: "application/json" }
2365
+ return { validatedValues, secretValues, errors: [] };
2366
+ }
2367
+ async function applyAssetItem(presetRef, resolved, item, inputs, opts, pinPolicy) {
2368
+ const { library } = opts;
2369
+ const validated = validateAssetPlaceholders(item, inputs);
2370
+ if (validated.errors.length > 0) {
2371
+ return { instances: [], assignments: [], errors: validated.errors };
2372
+ }
2373
+ const credentialRefPrefix = `preset/${presetRef}/${item.id ?? resolved.defRef}`;
2374
+ const context = {
2375
+ inputs: validated.validatedValues,
2376
+ secrets: validated.secretValues
2377
+ };
2378
+ const interpolated = interpolate(
2379
+ item.materialize,
2380
+ item.config,
2381
+ context,
2382
+ credentialRefPrefix
2383
+ );
2384
+ if (opts.secretsRouter && interpolated.secretsToStore.length > 0) {
2385
+ try {
2386
+ await storeSecrets(opts.secretsRouter, interpolated.secretsToStore);
2387
+ } catch (err) {
2388
+ return {
2389
+ instances: [],
2390
+ assignments: [],
2391
+ errors: [
2392
+ `Secret storage failed for "${item.ref}": ${err instanceof Error ? err.message : String(err)}`
2393
+ ]
2394
+ };
2395
+ }
2396
+ }
2397
+ try {
2398
+ const instance = await library.createInstance({
2399
+ defRef: resolved.defRef,
2400
+ defPin: pinPolicy,
2401
+ config: interpolated.config,
2402
+ credentialRef: interpolated.credentialRef,
2403
+ createdBy: opts.createdBy ?? `preset:${presetRef}`
2397
2404
  });
2398
- if (!res.ok) {
2399
- throw new CatalogHttpError(
2400
- `catalog.list failed: ${res.status} ${res.statusText}`,
2401
- res.status,
2402
- url
2403
- );
2405
+ const assignments2 = [];
2406
+ try {
2407
+ const asgn = await library.assign(opts.workspaceId, instance.id, pinPolicy);
2408
+ assignments2.push(asgn);
2409
+ } catch {
2404
2410
  }
2405
- const data = await parseTrpcGet(res);
2406
- if (!Array.isArray(data)) {
2407
- throw new CatalogHttpError(
2408
- `catalog.list returned non-array payload (got ${typeof data})`,
2409
- 200,
2410
- url
2411
- );
2411
+ return { instances: [instance], assignments: assignments2, errors: [] };
2412
+ } catch (err) {
2413
+ return {
2414
+ instances: [],
2415
+ assignments: [],
2416
+ errors: [
2417
+ `Instance creation failed for "${item.ref}": ${err instanceof Error ? err.message : String(err)}`
2418
+ ]
2419
+ };
2420
+ }
2421
+ }
2422
+
2423
+ // library/src/preset/upgrade.ts
2424
+ function detectChanges(oldManifest, newManifest) {
2425
+ const oldItems = indexItems(oldManifest.items);
2426
+ const newItems = indexItems(newManifest.items);
2427
+ const addedItems = [];
2428
+ const removedItems = [];
2429
+ const changedRefItems = [];
2430
+ const configChangedItems = [];
2431
+ for (const [key, oldItem] of oldItems) {
2432
+ if (!newItems.has(key)) {
2433
+ removedItems.push(oldItem);
2412
2434
  }
2413
- return data.map(normalizeCatalogAsset);
2414
2435
  }
2415
- };
2416
- function trpcGetUrl(baseUrl, procedure, input) {
2417
- const wrapped = JSON.stringify({ json: input });
2418
- return `${baseUrl.replace(/\/+$/, "")}/trpc/${procedure}?input=${encodeURIComponent(wrapped)}`;
2436
+ for (const [key, newItem] of newItems) {
2437
+ if (!oldItems.has(key)) {
2438
+ addedItems.push(newItem);
2439
+ }
2440
+ }
2441
+ for (const [key, newItem] of newItems) {
2442
+ const oldItem = oldItems.get(key);
2443
+ if (!oldItem) continue;
2444
+ if (oldItem.ref !== newItem.ref) {
2445
+ changedRefItems.push({ old: oldItem, new: newItem });
2446
+ } else if (!deepEqual(oldItem.placeholders, newItem.placeholders) || !deepEqual(oldItem.materialize, newItem.materialize)) {
2447
+ changedRefItems.push({ old: oldItem, new: newItem });
2448
+ } else if (!deepEqual(oldItem.config, newItem.config)) {
2449
+ configChangedItems.push({ old: oldItem, new: newItem });
2450
+ }
2451
+ }
2452
+ const isStructural = addedItems.length > 0 || removedItems.length > 0 || changedRefItems.length > 0;
2453
+ const kind = isStructural ? "structural" : "non-structural";
2454
+ const summaryParts = [];
2455
+ if (addedItems.length > 0) summaryParts.push(`+${addedItems.length} items`);
2456
+ if (removedItems.length > 0) summaryParts.push(`-${removedItems.length} items`);
2457
+ if (changedRefItems.length > 0) summaryParts.push(`${changedRefItems.length} ref changes`);
2458
+ if (configChangedItems.length > 0)
2459
+ summaryParts.push(`${configChangedItems.length} config changes`);
2460
+ const summary = summaryParts.length > 0 ? `${kind}: ${summaryParts.join(", ")}` : "no changes";
2461
+ return {
2462
+ kind,
2463
+ addedItems,
2464
+ removedItems,
2465
+ changedRefItems,
2466
+ configChangedItems,
2467
+ summary
2468
+ };
2419
2469
  }
2420
- async function parseTrpcGet(res) {
2421
- const text2 = await res.text();
2422
- let body;
2423
- try {
2424
- body = text2.length > 0 ? JSON.parse(text2) : null;
2425
- } catch {
2426
- throw new CatalogHttpError(
2427
- `tRPC response is not valid JSON: ${text2.slice(0, 200)}`,
2428
- res.status
2429
- );
2470
+ function evaluateUpgrade(oldManifest, newManifest) {
2471
+ const diff = detectChanges(oldManifest, newManifest);
2472
+ if (diff.kind === "non-structural") {
2473
+ return { autoUpgradable: true, diff };
2430
2474
  }
2431
- if (body && typeof body === "object" && "error" in body) {
2432
- const err = body.error;
2433
- throw new CatalogHttpError(
2434
- `tRPC error: ${err.message ?? "unknown"}${err.data?.code ? ` (${err.data.code})` : ""}`,
2435
- res.status
2475
+ const requiredActions = [];
2476
+ if (diff.addedItems.length > 0) {
2477
+ requiredActions.push(
2478
+ `Create ${diff.addedItems.length} new instance(s): ${diff.addedItems.map((i) => i.ref).join(", ")}`
2436
2479
  );
2437
2480
  }
2438
- const result = body?.result;
2439
- if (!result || !("data" in result)) {
2440
- throw new CatalogHttpError(`tRPC response missing result.data envelope`, res.status);
2481
+ if (diff.removedItems.length > 0) {
2482
+ requiredActions.push(
2483
+ `Remove ${diff.removedItems.length} instance(s): ${diff.removedItems.map((i) => i.ref).join(", ")}`
2484
+ );
2441
2485
  }
2442
- const data = result.data;
2443
- if (data && typeof data === "object" && !Array.isArray(data) && "json" in data) {
2444
- return data.json;
2486
+ if (diff.changedRefItems.length > 0) {
2487
+ requiredActions.push(`Replace ${diff.changedRefItems.length} instance(s) with new refs`);
2445
2488
  }
2446
- return data;
2489
+ return { autoUpgradable: false, diff, requiredActions };
2447
2490
  }
2448
- function normalizeCatalogAsset(raw) {
2449
- if (!raw || typeof raw !== "object") {
2450
- throw new CatalogHttpError("Catalog response item is not an object", 200);
2451
- }
2452
- const r = raw;
2453
- const publisher = pickString(r, "publisher") ?? pickString(r, "publisherSlug") ?? "";
2454
- const name = pickString(r, "name") ?? "";
2455
- const version = pickString(r, "version") ?? "";
2456
- const id = pickString(r, "id") ?? `${publisher}/${name}@${version}`;
2457
- const kind = pickString(r, "kind") ?? "";
2458
- const manifest = typeof r.manifest === "object" && r.manifest !== null ? r.manifest : typeof r.manifestJson === "object" && r.manifestJson !== null ? r.manifestJson : {};
2459
- const asset = {
2460
- id,
2461
- kind,
2462
- publisher,
2463
- name,
2464
- version,
2465
- manifest
2466
- };
2467
- const sha = pickString(r, "sha256") ?? pickString(r, "tarballSha256");
2468
- if (sha) asset.sha256 = sha;
2469
- const description = pickString(r, "description");
2470
- if (description) asset.description = description;
2471
- const license = pickString(r, "license");
2472
- if (license) asset.license = license;
2473
- const category = pickString(r, "category");
2474
- if (category) asset.category = category;
2475
- const repo = pickString(r, "repo");
2476
- if (repo) asset.repo = repo;
2477
- return asset;
2491
+ async function applyNonStructuralUpgrade(newManifest, existingInstances, library) {
2492
+ let updated = 0;
2493
+ const errors = [];
2494
+ for (const item of newManifest.items) {
2495
+ const key = itemKey(item);
2496
+ const instanceId = existingInstances.get(key);
2497
+ if (!instanceId) continue;
2498
+ if (item.config) {
2499
+ try {
2500
+ await library.updateInstance(instanceId, {
2501
+ config: item.config
2502
+ });
2503
+ updated++;
2504
+ } catch (err) {
2505
+ errors.push(
2506
+ `Failed to update instance "${instanceId}": ${err instanceof Error ? err.message : String(err)}`
2507
+ );
2508
+ }
2509
+ }
2510
+ }
2511
+ return { updated, errors };
2478
2512
  }
2479
- function pickString(r, key) {
2480
- const v = r[key];
2481
- return typeof v === "string" ? v : void 0;
2513
+ function itemKey(item) {
2514
+ return item.id ?? item.ref;
2482
2515
  }
2483
- var CATALOG_REQUEST_TIMEOUT_MS = 1e4;
2484
- var defaultFetch = async (url, init) => {
2485
- if (typeof fetch === "undefined") {
2486
- throw new Error("global fetch is not available; pass an httpClient explicitly");
2516
+ function indexItems(items) {
2517
+ const map = /* @__PURE__ */ new Map();
2518
+ for (const item of items) {
2519
+ map.set(itemKey(item), item);
2487
2520
  }
2488
- const requestInit = init ?? {};
2489
- return fetch(url, {
2490
- ...requestInit,
2491
- signal: requestInit.signal ?? AbortSignal.timeout(CATALOG_REQUEST_TIMEOUT_MS)
2492
- });
2493
- };
2494
-
2495
- // library/src/remote/rest-catalog-source.ts
2496
- var DEFAULT_TTL_MS2 = 864e5;
2497
- var RestCatalogSource = class {
2498
- id;
2499
- baseUrl;
2500
- cache;
2501
- cacheTtlMs;
2502
- httpClient;
2503
- silentBackgroundErrors;
2504
- /** Tracks in-flight background revalidations so callers can `await` them in tests. */
2505
- inFlight = /* @__PURE__ */ new Map();
2521
+ return map;
2522
+ }
2523
+ function deepEqual(a, b) {
2524
+ if (a === b) return true;
2525
+ if (a === void 0 && b === void 0) return true;
2526
+ if (a === null && b === null) return true;
2527
+ if (a === void 0 || b === void 0) return false;
2528
+ if (a === null || b === null) return false;
2529
+ return JSON.stringify(a) === JSON.stringify(b);
2530
+ }
2531
+ function defaultCacheDir() {
2532
+ return path5.join(os.homedir(), ".skaile", "catalog-cache");
2533
+ }
2534
+ var INDEX_FILE = "index.json";
2535
+ var TARBALL_DIR = "tarballs";
2536
+ var CatalogCache = class {
2537
+ dir;
2538
+ ttlMs;
2539
+ nowFn;
2540
+ index = null;
2506
2541
  constructor(opts) {
2507
- this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
2508
- this.id = `rest:${this.baseUrl}`;
2509
- this.cacheTtlMs = opts.cacheTtlMs ?? DEFAULT_TTL_MS2;
2510
- this.httpClient = opts.httpClient ?? defaultFetch2;
2511
- this.silentBackgroundErrors = opts.silentBackgroundErrors ?? false;
2512
- this.cache = opts.cache ?? new CatalogCache({
2513
- dir: opts.cacheDir,
2514
- ttlMs: this.cacheTtlMs,
2515
- now: opts.now
2516
- });
2542
+ this.dir = opts.dir ?? defaultCacheDir();
2543
+ this.ttlMs = opts.ttlMs;
2544
+ this.nowFn = opts.now ?? Date.now;
2545
+ this.ensureDir();
2517
2546
  }
2518
- // ── ICatalogSource ──────────────────────────────────────────────────────
2547
+ // ── public: resolve cache ───────────────────────────────────────────────
2519
2548
  /**
2520
- * Resolve a single asset by canonical ref (`<publisher>/<name>@<version>`).
2521
- *
2522
- * Cache: hit returns immediately. Miss fetches `/api/catalog/get?ref=<ref>`. Stale
2523
- * entry serves the cached value and triggers a background refresh.
2549
+ * Return a fresh resolve-cache entry for `ref`, or `null` if absent / stale.
2524
2550
  *
2525
- * Air-gapped: throws {@link OfflineError} on cache miss.
2551
+ * When `ttlMs === 0` (air-gapped), all entries are considered fresh
2552
+ * regardless of `fetchedAt`.
2526
2553
  */
2527
- async resolve(ref) {
2528
- const fresh = this.cache.getResolve(ref);
2529
- if (fresh) return fresh.value;
2530
- if (this.cache.airGapped) {
2531
- const stale2 = this.cache.getResolveStale(ref);
2532
- if (stale2) return stale2.value;
2533
- throw new OfflineError(`Asset '${ref}' not in cache`);
2534
- }
2535
- const stale = this.cache.getResolveStale(ref);
2536
- if (stale) {
2537
- this.scheduleBackground(`resolve:${ref}`, () => this.fetchAndStoreResolve(ref));
2538
- return stale.value;
2539
- }
2540
- return this.fetchAndStoreResolve(ref);
2554
+ getResolve(ref) {
2555
+ const idx = this.loadIndex();
2556
+ const entry = idx.resolve[ref];
2557
+ if (!entry) return null;
2558
+ if (this.ttlMs === 0) return entry;
2559
+ if (this.nowFn() - entry.fetchedAt > this.ttlMs) return null;
2560
+ return entry;
2541
2561
  }
2542
2562
  /**
2543
- * List assets matching `filter`.
2544
- *
2545
- * Cache: hit returns immediately. Miss fetches `/api/catalog/list`. Stale
2546
- * entry serves the cached array and triggers a background refresh.
2547
- *
2548
- * Air-gapped: throws {@link OfflineError} on cache miss.
2549
- *
2550
- * Note: `filter.prefix` is not supported by the forge-store REST API and is silently ignored.
2563
+ * Return a stale resolve-cache entry regardless of TTL — for stale-while-revalidate.
2551
2564
  */
2552
- async listAssets(filter) {
2553
- const fresh = this.cache.getList(filter);
2554
- if (fresh) return fresh.items;
2555
- if (this.cache.airGapped) {
2556
- const stale2 = this.cache.getListStale(filter);
2557
- if (stale2) return stale2.items;
2558
- throw new OfflineError("Catalog list not in cache");
2559
- }
2560
- const stale = this.cache.getListStale(filter);
2561
- if (stale) {
2562
- const filterKey2 = JSON.stringify(filter ?? null);
2563
- this.scheduleBackground(`list:${filterKey2}`, () => this.fetchAndStoreList(filter));
2564
- return stale.items;
2565
- }
2566
- return this.fetchAndStoreList(filter);
2565
+ getResolveStale(ref) {
2566
+ const idx = this.loadIndex();
2567
+ return idx.resolve[ref] ?? null;
2567
2568
  }
2568
- /**
2569
- * Fetch the content-addressed tarball for an asset.
2570
- *
2571
- * Steps:
2572
- * 1. `GET /api/tarball/manifest?sha256=<sha256>` → JSON with `{ url, ... }`
2573
- * 2. `GET <manifest.url>` following redirects → raw bytes
2574
- * 3. Verify SHA256 client-side — hard fail on mismatch
2575
- * 4. Write to cache on success
2576
- *
2577
- * Cache: tarballs are content-addressed and cached forever once written.
2578
- *
2579
- * Air-gapped: throws {@link OfflineError} on cache miss.
2580
- *
2581
- * @throws {@link TarballHashMismatchError} when the downloaded SHA256 does not match.
2582
- */
2583
- async fetchTarball(_ref, sha256) {
2584
- const cached = this.cache.readTarball(sha256);
2585
- if (cached) {
2586
- const actual2 = sha256Hex(cached);
2587
- if (actual2 !== sha256) {
2588
- throw new TarballHashMismatchError(sha256, actual2);
2589
- }
2590
- return cached;
2591
- }
2592
- if (this.cache.airGapped) {
2593
- throw new OfflineError(`Tarball '${sha256}' not in cache`);
2594
- }
2595
- const manifestUrl = `${this.baseUrl}/api/tarball/manifest?sha256=${encodeURIComponent(sha256)}`;
2596
- const manifestRes = await this.httpClient(manifestUrl, {
2597
- method: "GET",
2598
- headers: { Accept: "application/json" }
2599
- });
2600
- if (!manifestRes.ok) {
2601
- throw new CatalogHttpError(
2602
- `Failed to fetch tarball manifest for ${sha256}: ${manifestRes.status} ${manifestRes.statusText}`,
2603
- manifestRes.status,
2604
- manifestUrl
2605
- );
2606
- }
2607
- const manifestText = await manifestRes.text();
2608
- let manifest;
2609
- try {
2610
- manifest = JSON.parse(manifestText);
2611
- } catch {
2612
- throw new CatalogHttpError(
2613
- `Tarball manifest response is not valid JSON: ${manifestText.slice(0, 200)}`,
2614
- manifestRes.status,
2615
- manifestUrl
2616
- );
2617
- }
2618
- const downloadUrl = manifest && typeof manifest === "object" && "url" in manifest ? manifest.url : void 0;
2619
- if (typeof downloadUrl !== "string") {
2620
- throw new CatalogHttpError(
2621
- `Tarball manifest missing 'url' field`,
2622
- manifestRes.status,
2623
- manifestUrl
2624
- );
2625
- }
2626
- const tarballRes = await this.httpClient(downloadUrl, { method: "GET", redirect: "follow" });
2627
- if (!tarballRes.ok) {
2628
- throw new CatalogHttpError(
2629
- `Failed to fetch tarball ${sha256}: ${tarballRes.status} ${tarballRes.statusText}`,
2630
- tarballRes.status,
2631
- downloadUrl
2632
- );
2633
- }
2634
- const buf = new Uint8Array(await tarballRes.arrayBuffer());
2635
- const actual = sha256Hex(buf);
2636
- if (actual !== sha256) {
2637
- throw new TarballHashMismatchError(sha256, actual);
2638
- }
2639
- this.cache.writeTarball(sha256, buf);
2640
- return buf;
2569
+ /** Persist a resolve-cache entry (use `null` to negatively cache misses). */
2570
+ setResolve(ref, value) {
2571
+ const idx = this.loadIndex();
2572
+ idx.resolve[ref] = { fetchedAt: this.nowFn(), value };
2573
+ this.writeIndex(idx);
2641
2574
  }
2575
+ // ── public: list cache ──────────────────────────────────────────────────
2642
2576
  /**
2643
- * Enumerate available versions for a version-less ref. The forge-store REST
2644
- * API has no version-list route, so this derives versions from `listAssets`
2645
- * (filtered to the publisher) and keeps only the matching kind+name. Reuses
2646
- * the cached list path, so repeated calls are cheap.
2577
+ * Return a fresh list-cache entry for `filter`, or `null` if absent / stale.
2647
2578
  */
2648
- async listVersions(ref) {
2649
- const parsed = parseAssetRef(ref);
2650
- const wantPublisher = parsed.publisher?.replace(/^@/, "");
2651
- const assets = await this.listAssets(wantPublisher ? { publisher: wantPublisher } : void 0);
2652
- return assets.filter(
2653
- (a) => a.kind === parsed.kind && a.name === parsed.name && (wantPublisher === void 0 || a.publisher.replace(/^@/, "") === wantPublisher)
2654
- ).map((a) => a.version);
2579
+ getList(filter) {
2580
+ const key = filterKey(filter);
2581
+ const idx = this.loadIndex();
2582
+ const entry = idx.list[key];
2583
+ if (!entry) return null;
2584
+ if (this.ttlMs === 0) return entry;
2585
+ if (this.nowFn() - entry.fetchedAt > this.ttlMs) return null;
2586
+ return entry;
2587
+ }
2588
+ /** Return a stale list-cache entry regardless of TTL — for stale-while-revalidate. */
2589
+ getListStale(filter) {
2590
+ const key = filterKey(filter);
2591
+ const idx = this.loadIndex();
2592
+ return idx.list[key] ?? null;
2593
+ }
2594
+ /** Persist a list-cache entry. */
2595
+ setList(filter, items) {
2596
+ const key = filterKey(filter);
2597
+ const idx = this.loadIndex();
2598
+ idx.list[key] = { fetchedAt: this.nowFn(), items };
2599
+ this.writeIndex(idx);
2600
+ }
2601
+ // ── public: tarball cache ───────────────────────────────────────────────
2602
+ /** Absolute path where a tarball with `sha256` would be cached. */
2603
+ tarballPath(sha256) {
2604
+ return path5.join(this.dir, TARBALL_DIR, `${sha256}.tgz`);
2605
+ }
2606
+ /** Read a cached tarball from disk if present, otherwise `null`. */
2607
+ readTarball(sha256) {
2608
+ const p = this.tarballPath(sha256);
2609
+ if (!fs10.existsSync(p)) return null;
2610
+ return fs10.readFileSync(p);
2611
+ }
2612
+ /** Persist a tarball blob to disk (atomic write). */
2613
+ writeTarball(sha256, bytes) {
2614
+ const dir = path5.join(this.dir, TARBALL_DIR);
2615
+ if (!fs10.existsSync(dir)) fs10.mkdirSync(dir, { recursive: true });
2616
+ const finalPath = this.tarballPath(sha256);
2617
+ const tmpPath = `${finalPath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
2618
+ fs10.writeFileSync(tmpPath, bytes);
2619
+ fs10.renameSync(tmpPath, finalPath);
2655
2620
  }
2621
+ // ── public: invalidation ────────────────────────────────────────────────
2656
2622
  /**
2657
- * Single synthetic Source pointing at the configured forge-store URL.
2623
+ * Wipe all cached metadata (resolve + list). Tarballs are content-addressed
2624
+ * and remain valid — they are not invalidated.
2625
+ *
2626
+ * Called by `skaile update`.
2658
2627
  */
2659
- async listSources() {
2660
- return [{ id: this.id, type: "rest-catalog", url: this.baseUrl }];
2628
+ invalidateAll() {
2629
+ const idx = { version: 1, resolve: {}, list: {} };
2630
+ this.index = idx;
2631
+ this.writeIndex(idx);
2661
2632
  }
2662
- // ── extension API ───────────────────────────────────────────────────────
2663
2633
  /**
2664
- * Force a metadata-cache refresh. Wipes all resolve/list entries and
2665
- * re-fetches `listAssets()` (no filter) to pre-warm the canonical query.
2666
- *
2667
- * Driven by `skaile update`. Tarballs (content-addressed) are not touched.
2668
- *
2669
- * In air-gapped mode this method **does** make a network call (bypassing
2670
- * the TTL=0 gate); it is the only way to repopulate the cache.
2634
+ * Wipe all cached metadata **and** delete tarball blobs. Used for full reset.
2671
2635
  */
2672
- async refresh() {
2673
- this.cache.invalidateAll();
2674
- const items = await this.fetchListNetwork(void 0);
2675
- this.cache.setList(void 0, items);
2676
- for (const item of items) {
2677
- this.cache.setResolve(item.id, item);
2636
+ invalidateEverything() {
2637
+ this.invalidateAll();
2638
+ const tarballDir = path5.join(this.dir, TARBALL_DIR);
2639
+ if (fs10.existsSync(tarballDir)) {
2640
+ fs10.rmSync(tarballDir, { recursive: true, force: true });
2678
2641
  }
2679
- return { assetsCached: items.length };
2680
2642
  }
2681
- /**
2682
- * Wait for any in-flight stale-while-revalidate background fetches to settle.
2683
- * Test-only escape hatch.
2684
- */
2685
- async waitForBackground() {
2686
- const promises = Array.from(this.inFlight.values());
2687
- await Promise.allSettled(promises);
2643
+ /** Return whether `ttlMs` is zero (air-gapped mode). */
2644
+ get airGapped() {
2645
+ return this.ttlMs === 0;
2688
2646
  }
2689
2647
  // ── private ─────────────────────────────────────────────────────────────
2690
- scheduleBackground(key, fn) {
2691
- if (this.inFlight.has(key)) return;
2692
- const p = (async () => {
2693
- try {
2694
- await fn();
2695
- } catch (err) {
2696
- if (!this.silentBackgroundErrors) {
2697
- process.stderr.write(
2698
- `[skaile/catalog] Background refresh failed for ${key}: ${err instanceof Error ? err.message : String(err)}
2699
- `
2700
- );
2701
- }
2702
- } finally {
2703
- this.inFlight.delete(key);
2704
- }
2705
- })();
2706
- this.inFlight.set(key, p);
2707
- }
2708
- async fetchAndStoreResolve(ref) {
2709
- const value = await this.fetchResolveNetwork(ref);
2710
- this.cache.setResolve(ref, value);
2711
- return value;
2712
- }
2713
- async fetchAndStoreList(filter) {
2714
- const items = await this.fetchListNetwork(filter);
2715
- this.cache.setList(filter, items);
2716
- return items;
2648
+ ensureDir() {
2649
+ if (!fs10.existsSync(this.dir)) fs10.mkdirSync(this.dir, { recursive: true });
2650
+ const tarballDir = path5.join(this.dir, TARBALL_DIR);
2651
+ if (!fs10.existsSync(tarballDir)) fs10.mkdirSync(tarballDir, { recursive: true });
2717
2652
  }
2718
- /** Raw network call: GET /api/catalog/get?ref=<ref> */
2719
- async fetchResolveNetwork(ref) {
2720
- const url = `${this.baseUrl}/api/catalog/get?ref=${encodeURIComponent(ref)}`;
2721
- const res = await this.httpClient(url, {
2722
- method: "GET",
2723
- headers: { Accept: "application/json" }
2724
- });
2725
- if (!res.ok) {
2726
- throw new CatalogHttpError(
2727
- `catalog/get ${ref} failed: ${res.status} ${res.statusText}`,
2728
- res.status,
2729
- url
2730
- );
2731
- }
2732
- const data = await parseRestResponse(res, url);
2733
- if (data === null || data === void 0) return null;
2734
- return normalizeRestAsset(data);
2653
+ indexPath() {
2654
+ return path5.join(this.dir, INDEX_FILE);
2735
2655
  }
2736
- /** Raw network call: GET /api/catalog/list?publisher=&kind=&category= */
2737
- async fetchListNetwork(filter) {
2738
- const params = new URLSearchParams();
2739
- if (filter?.publisher) params.set("publisher", filter.publisher);
2740
- if (filter?.kind) params.set("kind", filter.kind);
2741
- const qs = params.toString();
2742
- const url = `${this.baseUrl}/api/catalog/list${qs ? `?${qs}` : ""}`;
2743
- const res = await this.httpClient(url, {
2744
- method: "GET",
2745
- headers: { Accept: "application/json" }
2746
- });
2747
- if (!res.ok) {
2748
- throw new CatalogHttpError(
2749
- `catalog/list failed: ${res.status} ${res.statusText}`,
2750
- res.status,
2751
- url
2752
- );
2753
- }
2754
- const data = await parseRestResponse(res, url);
2755
- if (!Array.isArray(data)) {
2756
- throw new CatalogHttpError(
2757
- `catalog/list returned non-array payload (got ${typeof data})`,
2758
- 200,
2759
- url
2760
- );
2656
+ loadIndex() {
2657
+ if (this.index) return this.index;
2658
+ const p = this.indexPath();
2659
+ if (!fs10.existsSync(p)) {
2660
+ this.index = { version: 1, resolve: {}, list: {} };
2661
+ return this.index;
2761
2662
  }
2762
- return data.map(normalizeRestAsset);
2763
- }
2764
- };
2765
- async function parseRestResponse(res, url) {
2766
- const text2 = await res.text();
2767
- if (text2.length === 0) return null;
2768
- try {
2769
- return JSON.parse(text2);
2770
- } catch {
2771
- throw new CatalogHttpError(
2772
- `REST response is not valid JSON: ${text2.slice(0, 200)}`,
2773
- res.status,
2774
- url
2775
- );
2776
- }
2777
- }
2778
- function normalizeRestAsset(raw) {
2779
- if (!raw || typeof raw !== "object") {
2780
- throw new CatalogHttpError("Catalog REST response item is not an object", 200);
2781
- }
2782
- const r = raw;
2783
- const publisher = pickString2(r, "publisherNamespace") ?? pickString2(r, "publisher") ?? "";
2784
- const name = pickString2(r, "name") ?? "";
2785
- const version = pickString2(r, "version") ?? "";
2786
- const kind = pickString2(r, "kind") ?? "";
2787
- const id = `${publisher}/${name}@${version}`;
2788
- const requiresParsed = tryParseJson(pickString2(r, "requiresJson"));
2789
- const filesParsed = tryParseJson(pickString2(r, "filesJson"));
2790
- const manifest = { ...requiresParsed, ...filesParsed };
2791
- const asset = {
2792
- id,
2793
- kind,
2794
- publisher,
2795
- name,
2796
- version,
2797
- manifest
2798
- };
2799
- const sha = pickString2(r, "sha256");
2800
- if (sha) asset.sha256 = sha;
2801
- const description = pickString2(r, "description");
2802
- if (description) asset.description = description;
2803
- const license = pickString2(r, "license");
2804
- if (license) asset.license = license;
2805
- const category = pickString2(r, "category");
2806
- if (category) asset.category = category;
2807
- return asset;
2808
- }
2809
- function pickString2(r, key) {
2810
- const v = r[key];
2811
- return typeof v === "string" ? v : void 0;
2812
- }
2813
- function tryParseJson(s) {
2814
- if (!s) return {};
2815
- try {
2816
- const parsed = JSON.parse(s);
2817
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
2818
- return parsed;
2663
+ try {
2664
+ const raw = JSON.parse(fs10.readFileSync(p, "utf-8"));
2665
+ if (raw && typeof raw === "object" && raw.version === 1) {
2666
+ this.index = {
2667
+ version: 1,
2668
+ resolve: raw.resolve ?? {},
2669
+ list: raw.list ?? {}
2670
+ };
2671
+ return this.index;
2672
+ }
2673
+ } catch {
2819
2674
  }
2820
- } catch {
2675
+ this.index = { version: 1, resolve: {}, list: {} };
2676
+ return this.index;
2821
2677
  }
2822
- return {};
2823
- }
2824
- var CATALOG_REQUEST_TIMEOUT_MS2 = 1e4;
2825
- var defaultFetch2 = async (url, init) => {
2826
- if (typeof fetch === "undefined") {
2827
- throw new Error("global fetch is not available; pass an httpClient explicitly");
2678
+ writeIndex(idx) {
2679
+ this.index = idx;
2680
+ const p = this.indexPath();
2681
+ const tmp = `${p}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
2682
+ fs10.writeFileSync(tmp, JSON.stringify(idx, null, 2), "utf-8");
2683
+ fs10.renameSync(tmp, p);
2828
2684
  }
2829
- const requestInit = init ?? {};
2830
- return fetch(url, {
2831
- ...requestInit,
2832
- signal: requestInit.signal ?? AbortSignal.timeout(CATALOG_REQUEST_TIMEOUT_MS2)
2833
- });
2834
2685
  };
2835
- var KnowledgeManifestSchema = z2.looseObject({
2836
- name: z2.string().optional(),
2837
- description: z2.string().optional(),
2838
- version: z2.string().optional(),
2839
- files: z2.array(z2.string()).optional(),
2840
- keywords: z2.array(z2.string()).optional(),
2841
- license: z2.string().optional()
2842
- });
2843
- function validateKnowledge(data) {
2844
- const result = KnowledgeManifestSchema.safeParse(data);
2845
- if (result.success) {
2846
- return { ok: true, data: result.data };
2847
- }
2848
- return {
2849
- ok: false,
2850
- errors: result.error.issues.map((issue) => ({
2851
- path: issue.path.join("."),
2852
- message: issue.message
2853
- }))
2854
- };
2855
- }
2856
- function collectDirRecursive(baseDir, relativeDir) {
2857
- const full = path4.join(baseDir, relativeDir);
2858
- if (!fs11.existsSync(full) || !fs11.statSync(full).isDirectory()) return [];
2859
- const results = [];
2860
- for (const entry of fs11.readdirSync(full, { withFileTypes: true })) {
2861
- const rel = path4.posix.join(relativeDir, entry.name);
2862
- if (entry.isDirectory()) {
2863
- results.push(...collectDirRecursive(baseDir, rel));
2864
- } else if (entry.isFile()) {
2865
- results.push(rel);
2866
- }
2686
+ function filterKey(filter) {
2687
+ if (!filter) return "__all__";
2688
+ const keys = Object.keys(filter).sort();
2689
+ const norm = {};
2690
+ for (const k of keys) {
2691
+ const v = filter[k];
2692
+ if (v !== void 0) norm[k] = v;
2867
2693
  }
2868
- return results;
2869
- }
2870
- var knowledgeKindProvider = {
2871
- kind: "knowledge",
2872
- displayName: "Knowledge",
2873
- providerVersion: "1.0.0",
2874
- discoveryPatterns: ["**/knowledge.yaml", "**/knowledge/index.yaml"],
2875
- matchesPath(relativePath) {
2876
- const parts = relativePath.split("/");
2877
- const filename = parts[parts.length - 1] ?? "";
2878
- if (filename === "knowledge.yaml") return true;
2879
- if (filename === "index.yaml" && parts.length >= 2 && parts[parts.length - 2] === "knowledge") {
2880
- return true;
2881
- }
2882
- return false;
2883
- },
2884
- validateManifest(data) {
2885
- return validateKnowledge(data);
2886
- },
2887
- defaultFileFilter(manifestPath) {
2888
- const dir = path4.dirname(manifestPath);
2889
- const manifestName = path4.basename(manifestPath);
2890
- const files = [manifestName];
2891
- files.push(...collectDirRecursive(dir, "documents"));
2892
- files.sort();
2893
- return { assetRoot: dir, files };
2694
+ return Object.keys(norm).length === 0 ? "__all__" : JSON.stringify(norm);
2695
+ }
2696
+ function sha256Hex(bytes) {
2697
+ const h = crypto.createHash("sha256");
2698
+ h.update(bytes);
2699
+ return h.digest("hex");
2700
+ }
2701
+
2702
+ // library/src/remote/remote-catalog-source.ts
2703
+ var TarballHashMismatchError = class extends Error {
2704
+ constructor(expected, actual) {
2705
+ super(`SHA256 mismatch: expected ${expected}, got ${actual}`);
2706
+ this.expected = expected;
2707
+ this.actual = actual;
2708
+ this.name = "TarballHashMismatchError";
2894
2709
  }
2895
- // Knowledge has no cross-asset dependency edges — omit extractRequires
2710
+ expected;
2711
+ actual;
2896
2712
  };
2897
- function parsePreset(data) {
2898
- const zodResult = PresetManifestSchema.safeParse(data);
2899
- if (!zodResult.success) {
2900
- return {
2901
- ok: false,
2902
- errors: zodResult.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
2903
- };
2713
+ var OfflineError = class extends Error {
2714
+ constructor(message) {
2715
+ super(`${message} (air-gapped mode; run \`skaile update\` to refresh the cache)`);
2716
+ this.name = "OfflineError";
2904
2717
  }
2905
- const preset = zodResult.data;
2906
- const errors = [];
2907
- const ids = /* @__PURE__ */ new Set();
2908
- const refCounts = /* @__PURE__ */ new Map();
2909
- for (const item of preset.items) {
2910
- if (item.id) {
2911
- if (ids.has(item.id)) {
2912
- errors.push(`Duplicate item id: "${item.id}"`);
2913
- }
2914
- ids.add(item.id);
2915
- } else {
2916
- refCounts.set(item.ref, (refCounts.get(item.ref) ?? 0) + 1);
2917
- }
2718
+ };
2719
+ var CatalogHttpError = class extends Error {
2720
+ constructor(message, status, url) {
2721
+ super(message);
2722
+ this.status = status;
2723
+ this.url = url;
2724
+ this.name = "CatalogHttpError";
2918
2725
  }
2919
- for (const [ref, count] of refCounts) {
2920
- if (count > 1) {
2921
- errors.push(
2922
- `${count} items share ref "${ref}" without distinct "id" fields \u2014 upgrade matching will be ambiguous`
2923
- );
2726
+ status;
2727
+ url;
2728
+ };
2729
+ var DEFAULT_TTL_MS = 864e5;
2730
+ var RemoteCatalogSource = class {
2731
+ id;
2732
+ baseUrl;
2733
+ cache;
2734
+ cacheTtlMs;
2735
+ httpClient;
2736
+ silentBackgroundErrors;
2737
+ /** Tracks in-flight background revalidations so callers can `await` them in tests. */
2738
+ inFlight = /* @__PURE__ */ new Map();
2739
+ constructor(opts) {
2740
+ this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
2741
+ this.id = `remote:${this.baseUrl}`;
2742
+ this.cacheTtlMs = opts.cacheTtlMs ?? DEFAULT_TTL_MS;
2743
+ this.httpClient = opts.httpClient ?? defaultFetch;
2744
+ this.silentBackgroundErrors = opts.silentBackgroundErrors ?? false;
2745
+ this.cache = opts.cache ?? new CatalogCache({
2746
+ dir: opts.cacheDir,
2747
+ ttlMs: this.cacheTtlMs,
2748
+ now: opts.now
2749
+ });
2750
+ }
2751
+ // ── ICatalogSource ──────────────────────────────────────────────────────
2752
+ /**
2753
+ * Resolve a single asset by canonical ref (`<publisher>/<name>@<version>`).
2754
+ *
2755
+ * Cache: hit returns immediately. Miss fetches `/trpc/catalog.get`. Stale
2756
+ * entry serves the cached value and triggers a background refresh.
2757
+ *
2758
+ * Air-gapped: throws {@link OfflineError} on cache miss.
2759
+ */
2760
+ async resolve(ref) {
2761
+ const fresh = this.cache.getResolve(ref);
2762
+ if (fresh) return fresh.value;
2763
+ if (this.cache.airGapped) {
2764
+ const stale2 = this.cache.getResolveStale(ref);
2765
+ if (stale2) return stale2.value;
2766
+ throw new OfflineError(`Asset '${ref}' not in cache`);
2767
+ }
2768
+ const stale = this.cache.getResolveStale(ref);
2769
+ if (stale) {
2770
+ this.scheduleBackground(`resolve:${ref}`, () => this.fetchAndStoreResolve(ref));
2771
+ return stale.value;
2924
2772
  }
2773
+ return this.fetchAndStoreResolve(ref);
2925
2774
  }
2926
- for (const item of preset.items) {
2927
- if (item.composes) {
2928
- for (const compose of item.composes) {
2929
- if (compose.ref.startsWith("#")) {
2930
- const targetId = compose.ref.slice(1);
2931
- if (!ids.has(targetId)) {
2932
- errors.push(
2933
- `Cross-item ref "${compose.ref}" in item "${item.id ?? item.ref}" references undeclared id "${targetId}"`
2934
- );
2935
- }
2936
- }
2937
- }
2775
+ /**
2776
+ * List assets matching `filter`.
2777
+ *
2778
+ * Cache: hit returns immediately. Miss fetches `/trpc/catalog.list`. Stale
2779
+ * entry serves the cached array and triggers a background refresh.
2780
+ *
2781
+ * Air-gapped: throws {@link OfflineError} on cache miss.
2782
+ */
2783
+ async listAssets(filter) {
2784
+ const fresh = this.cache.getList(filter);
2785
+ if (fresh) return fresh.items;
2786
+ if (this.cache.airGapped) {
2787
+ const stale2 = this.cache.getListStale(filter);
2788
+ if (stale2) return stale2.items;
2789
+ throw new OfflineError("Catalog list not in cache");
2790
+ }
2791
+ const stale = this.cache.getListStale(filter);
2792
+ if (stale) {
2793
+ const filterKey2 = JSON.stringify(filter ?? null);
2794
+ this.scheduleBackground(`list:${filterKey2}`, () => this.fetchAndStoreList(filter));
2795
+ return stale.items;
2938
2796
  }
2797
+ return this.fetchAndStoreList(filter);
2939
2798
  }
2940
- for (const item of preset.items) {
2941
- if (item.placeholders) {
2942
- const keys = /* @__PURE__ */ new Set();
2943
- for (const ph of item.placeholders) {
2944
- if (keys.has(ph.key)) {
2945
- errors.push(`Duplicate placeholder key "${ph.key}" in item "${item.id ?? item.ref}"`);
2946
- }
2947
- keys.add(ph.key);
2948
- if (ph.type === "enum" && (!ph.values || ph.values.length === 0)) {
2949
- errors.push(`Placeholder "${ph.key}" has type "enum" but no values defined`);
2950
- }
2799
+ /**
2800
+ * Fetch the content-addressed tarball for an asset. The bytes are downloaded
2801
+ * via the REST endpoint `GET /tarball/:sha256` (which 307-redirects to the
2802
+ * storage tier), then verified against `sha256` on the client side.
2803
+ *
2804
+ * Cache: tarballs are content-addressed and cached forever once written.
2805
+ *
2806
+ * Air-gapped: throws {@link OfflineError} on cache miss.
2807
+ *
2808
+ * @throws {@link TarballHashMismatchError} when the downloaded SHA256 does not match.
2809
+ */
2810
+ async fetchTarball(_ref, sha256) {
2811
+ const cached = this.cache.readTarball(sha256);
2812
+ if (cached) {
2813
+ const actual2 = sha256Hex(cached);
2814
+ if (actual2 !== sha256) {
2815
+ throw new TarballHashMismatchError(sha256, actual2);
2951
2816
  }
2817
+ return cached;
2952
2818
  }
2953
- }
2954
- if (errors.length > 0) {
2955
- return { ok: false, preset, errors };
2956
- }
2957
- return { ok: true, preset, errors: [] };
2958
- }
2959
- function parsePresetYaml(yamlContent) {
2960
- let data;
2961
- try {
2962
- data = parse(yamlContent);
2963
- } catch (err) {
2964
- return {
2965
- ok: false,
2966
- errors: [`YAML parse error: ${err instanceof Error ? err.message : String(err)}`]
2967
- };
2968
- }
2969
- return parsePreset(data);
2970
- }
2971
- function detectNestedPresetRefs(preset, knownPresetRefs) {
2972
- const nested = [];
2973
- for (const item of preset.items) {
2974
- if (item.ref.startsWith("library://") || item.ref.startsWith("#")) continue;
2975
- if (knownPresetRefs?.has(item.ref)) {
2976
- nested.push(item.ref);
2819
+ if (this.cache.airGapped) {
2820
+ throw new OfflineError(`Tarball '${sha256}' not in cache`);
2821
+ }
2822
+ const url = `${this.baseUrl}/tarball/${encodeURIComponent(sha256)}`;
2823
+ const res = await this.httpClient(url, { method: "GET", redirect: "follow" });
2824
+ if (!res.ok) {
2825
+ throw new CatalogHttpError(
2826
+ `Failed to fetch tarball ${sha256}: ${res.status} ${res.statusText}`,
2827
+ res.status,
2828
+ url
2829
+ );
2830
+ }
2831
+ const buf = new Uint8Array(await res.arrayBuffer());
2832
+ const actual = sha256Hex(buf);
2833
+ if (actual !== sha256) {
2834
+ throw new TarballHashMismatchError(sha256, actual);
2977
2835
  }
2836
+ this.cache.writeTarball(sha256, buf);
2837
+ return buf;
2978
2838
  }
2979
- return nested;
2980
- }
2981
- function validateNestingDepth(presetRef, currentDepth) {
2982
- const MAX_DEPTH = 3;
2983
- if (currentDepth >= MAX_DEPTH) {
2984
- return `Preset "${presetRef}" exceeds maximum nesting depth (${MAX_DEPTH} levels allowed, depth ${currentDepth} reached)`;
2839
+ /**
2840
+ * Enumerate available versions for a version-less ref. The Catalog has no
2841
+ * dedicated version-list route, so this derives versions from `listAssets`
2842
+ * (filtered to the publisher) and keeps only the matching kind+name. Reuses
2843
+ * the cached list path, so repeated calls are cheap.
2844
+ */
2845
+ async listVersions(ref) {
2846
+ const parsed = parseAssetRef(ref);
2847
+ const wantPublisher = parsed.publisher?.replace(/^@/, "");
2848
+ const assets = await this.listAssets(wantPublisher ? { publisher: wantPublisher } : void 0);
2849
+ return assets.filter(
2850
+ (a) => a.kind === parsed.kind && a.name === parsed.name && (wantPublisher === void 0 || a.publisher.replace(/^@/, "") === wantPublisher)
2851
+ ).map((a) => a.version);
2985
2852
  }
2986
- return void 0;
2987
- }
2988
-
2989
- // library/src/preset/resolve-item.ts
2990
- function classifyRef(ref) {
2991
- if (ref.startsWith("library://")) return "library-instance";
2992
- if (ref.startsWith("#")) return "cross-item";
2993
- return "asset";
2994
- }
2995
- function extractLibraryInstanceId(ref) {
2996
- const match = ref.match(/^library:\/\/[^/]+\/instances\/([a-f0-9-]+)$/i);
2997
- return match ? match[1] : null;
2998
- }
2999
- async function resolveItem(item, library, knownPresetRefs) {
3000
- const ref = item.ref;
3001
- if (ref.startsWith("#")) {
3002
- return { kind: "cross-item", item, targetId: ref.slice(1) };
2853
+ /**
2854
+ * Single synthetic Source pointing at the configured Catalog URL. Phase 2
2855
+ * is single-Source per spec; multi-source federation is Phase 4.8.
2856
+ */
2857
+ async listSources() {
2858
+ return [{ id: this.id, type: "remote-catalog", url: this.baseUrl }];
3003
2859
  }
3004
- if (ref.startsWith("library://")) {
3005
- const instanceId = extractLibraryInstanceId(ref);
3006
- if (!instanceId) {
3007
- return { kind: "library-instance", item, instanceId: "", instance: null };
2860
+ // ── extension API ───────────────────────────────────────────────────────
2861
+ /**
2862
+ * Force a metadata-cache refresh. Wipes all resolve/list entries and
2863
+ * re-fetches `listAssets()` (no filter) to pre-warm the canonical query.
2864
+ *
2865
+ * Driven by `skaile update`. Tarballs (content-addressed) are not touched
2866
+ * — they remain valid across refreshes.
2867
+ *
2868
+ * In air-gapped mode this method **does** make a network call (bypassing
2869
+ * the TTL=0 gate); it is the only way to repopulate the cache.
2870
+ */
2871
+ async refresh() {
2872
+ this.cache.invalidateAll();
2873
+ const items = await this.fetchListNetwork(void 0);
2874
+ this.cache.setList(void 0, items);
2875
+ for (const item of items) {
2876
+ this.cache.setResolve(item.id, item);
3008
2877
  }
3009
- const instance = await library.getInstance(instanceId);
3010
- return { kind: "library-instance", item, instanceId, instance };
3011
- }
3012
- if (knownPresetRefs?.has(ref)) {
3013
- return { kind: "nested-preset", item, presetRef: ref };
2878
+ return { assetsCached: items.length };
3014
2879
  }
3015
- const assetDef = await library.getAssetDef(ref);
3016
- return { kind: "asset", item, defRef: ref, assetDef };
3017
- }
3018
- async function resolveAllItems(items, library, knownPresetRefs) {
3019
- return Promise.all(items.map((item) => resolveItem(item, library, knownPresetRefs)));
3020
- }
3021
-
3022
- // library/src/preset/placeholders.ts
3023
- function validatePlaceholder(placeholder, value) {
3024
- const { key, type = "text", required } = placeholder;
3025
- if (value === void 0 || value === null || value === "") {
3026
- if (placeholder.default !== void 0) {
3027
- return { valid: true, errors: [], value: placeholder.default };
2880
+ /**
2881
+ * Fetch the pointer-only install manifest for an asset ref.
2882
+ *
2883
+ * The install manifest is the Pointer Triple: the immutable upstream
2884
+ * `source.url` + `source.commitSha`, the per-file `files[].sha256` list, a
2885
+ * composite `sha256` rollup, plus `kind` and `tier`. Callers fetch the asset
2886
+ * bytes directly from the upstream repo at that commit and verify each file
2887
+ * against the recorded hashes — the Catalog itself never serves asset bytes.
2888
+ *
2889
+ * Wire surface: `GET /trpc/catalog.getInstallManifest?input=<superjson({ ref })>`.
2890
+ *
2891
+ * This is a `RemoteCatalogSource`-only method — it is not part of the
2892
+ * `ICatalogSource` contract. `LocalCatalogSource` has the asset bytes on disk
2893
+ * and has no pointer-triple concept, so adding it to the shared interface
2894
+ * would force a meaningless implementation there.
2895
+ *
2896
+ * Not cached: the install path is a one-shot read driven by `skaile install`.
2897
+ *
2898
+ * @throws {@link CatalogHttpError} on a non-2xx response or malformed envelope.
2899
+ */
2900
+ async getInstallManifest(ref) {
2901
+ const url = trpcGetUrl(this.baseUrl, "catalog.getInstallManifest", { ref });
2902
+ const res = await this.httpClient(url, {
2903
+ method: "GET",
2904
+ headers: { Accept: "application/json" }
2905
+ });
2906
+ if (!res.ok) {
2907
+ throw new CatalogHttpError(
2908
+ `catalog.getInstallManifest ${ref} failed: ${res.status} ${res.statusText}`,
2909
+ res.status,
2910
+ url
2911
+ );
3028
2912
  }
3029
- if (required === true || required === void 0) {
3030
- return { valid: false, errors: [`Placeholder "${key}" is required`] };
2913
+ const data = await parseTrpcGet(res);
2914
+ if (data === null || data === void 0 || typeof data !== "object") {
2915
+ throw new CatalogHttpError(
2916
+ `catalog.getInstallManifest ${ref} returned a non-object payload (got ${typeof data})`,
2917
+ 200,
2918
+ url
2919
+ );
3031
2920
  }
3032
- return { valid: true, errors: [], value: void 0 };
3033
- }
3034
- switch (type) {
3035
- case "text":
3036
- return validateText(key, value);
3037
- case "multiline":
3038
- return validateMultiline(key, value);
3039
- case "secret":
3040
- return validateSecret(key, value);
3041
- case "number":
3042
- return validateNumber(key, value);
3043
- case "boolean":
3044
- return validateBoolean(key, value);
3045
- case "enum":
3046
- return validateEnum(key, value, placeholder.values);
3047
- case "oauth":
3048
- return validateOauth(key, value);
3049
- case "instance-picker":
3050
- return validateInstancePicker(key, value);
2921
+ return data;
3051
2922
  }
3052
- }
3053
- function validateAllPlaceholders(placeholders, inputs) {
3054
- const values = {};
3055
- const errors = [];
3056
- for (const ph of placeholders) {
3057
- const result = validatePlaceholder(ph, inputs[ph.key]);
3058
- if (!result.valid) {
3059
- errors.push(...result.errors);
3060
- } else if (result.value !== void 0) {
3061
- values[ph.key] = result.value;
2923
+ /**
2924
+ * Cheap probe used during cross-check when both a source-side and a store-side
2925
+ * candidate at the same version need to be verified for sha256 equality.
2926
+ * Returns only the composite sha256 — no source URL, no file list.
2927
+ *
2928
+ * Wire: `GET /trpc/catalog.getCanonicalDigest?input=<superjson({ ref })>`.
2929
+ *
2930
+ * Not cached: one-shot read during install resolution.
2931
+ *
2932
+ * @param ref - Canonical asset ref `<publisher>/<kind>:<name>@<version>`.
2933
+ * @returns `{ sha256 }` on a 200, or `null` on a 404.
2934
+ * @throws {@link CatalogHttpError} on a non-2xx/non-404 response or malformed payload.
2935
+ * @docLink packages/library/api-reference#remote-catalog-source
2936
+ */
2937
+ async getCanonicalDigest(ref) {
2938
+ const url = trpcGetUrl(this.baseUrl, "catalog.getCanonicalDigest", { ref });
2939
+ const res = await this.httpClient(url, {
2940
+ method: "GET",
2941
+ headers: { Accept: "application/json" }
2942
+ });
2943
+ if (res.status === 404) return null;
2944
+ if (!res.ok) {
2945
+ throw new CatalogHttpError(
2946
+ `catalog.getCanonicalDigest ${ref} failed: ${res.status} ${res.statusText}`,
2947
+ res.status,
2948
+ url
2949
+ );
3062
2950
  }
2951
+ const data = await parseTrpcGet(res);
2952
+ if (!data || typeof data !== "object") {
2953
+ throw new CatalogHttpError(
2954
+ `catalog.getCanonicalDigest ${ref} returned a non-object payload`,
2955
+ 200,
2956
+ url
2957
+ );
2958
+ }
2959
+ const d = data;
2960
+ if (typeof d.sha256 !== "string") {
2961
+ throw new CatalogHttpError(
2962
+ `catalog.getCanonicalDigest ${ref} missing sha256 string`,
2963
+ 200,
2964
+ url
2965
+ );
2966
+ }
2967
+ return { sha256: d.sha256 };
3063
2968
  }
3064
- return { valid: errors.length === 0, values, errors };
3065
- }
3066
- async function queryInstancePickerChoices(library, options) {
3067
- const filter = {};
3068
- if (options?.kind) {
3069
- filter.kind = options.kind;
3070
- }
3071
- if (options?.defRefPrefix) {
3072
- filter.defRef = options.defRefPrefix;
3073
- }
3074
- const instances2 = await library.listInstances(filter);
3075
- return instances2.map((inst) => ({
3076
- instanceId: inst.id,
3077
- defRef: inst.defRef,
3078
- label: `${inst.defRef} (${inst.id.slice(0, 8)})`
3079
- }));
3080
- }
3081
- function getDefaultValue(placeholder) {
3082
- return placeholder.default;
3083
- }
3084
- function isSecretPlaceholder(placeholder) {
3085
- return placeholder.type === "secret" || placeholder.type === "oauth";
3086
- }
3087
- function validateText(key, value) {
3088
- if (typeof value !== "string") {
3089
- return { valid: false, errors: [`Placeholder "${key}" must be a string`] };
3090
- }
3091
- return { valid: true, errors: [], value };
3092
- }
3093
- function validateMultiline(key, value) {
3094
- if (typeof value !== "string") {
3095
- return { valid: false, errors: [`Placeholder "${key}" must be a string`] };
2969
+ /**
2970
+ * Wait for any in-flight stale-while-revalidate background fetches to settle.
2971
+ * Test-only escape hatch.
2972
+ */
2973
+ async waitForBackground() {
2974
+ const promises = Array.from(this.inFlight.values());
2975
+ await Promise.allSettled(promises);
3096
2976
  }
3097
- return { valid: true, errors: [], value };
3098
- }
3099
- function validateSecret(key, value) {
3100
- if (typeof value !== "string") {
3101
- return { valid: false, errors: [`Placeholder "${key}" must be a string`] };
2977
+ // ── private ─────────────────────────────────────────────────────────────
2978
+ scheduleBackground(key, fn) {
2979
+ if (this.inFlight.has(key)) return;
2980
+ const p = (async () => {
2981
+ try {
2982
+ await fn();
2983
+ } catch (err) {
2984
+ if (!this.silentBackgroundErrors) {
2985
+ process.stderr.write(
2986
+ `[skaile/catalog] Background refresh failed for ${key}: ${err instanceof Error ? err.message : String(err)}
2987
+ `
2988
+ );
2989
+ }
2990
+ } finally {
2991
+ this.inFlight.delete(key);
2992
+ }
2993
+ })();
2994
+ this.inFlight.set(key, p);
3102
2995
  }
3103
- if (value.length === 0) {
3104
- return { valid: false, errors: [`Placeholder "${key}" (secret) cannot be empty`] };
2996
+ async fetchAndStoreResolve(ref) {
2997
+ const value = await this.fetchResolveNetwork(ref);
2998
+ this.cache.setResolve(ref, value);
2999
+ return value;
3105
3000
  }
3106
- return { valid: true, errors: [], value };
3107
- }
3108
- function validateNumber(key, value) {
3109
- if (typeof value === "number" && !Number.isNaN(value)) {
3110
- return { valid: true, errors: [], value };
3001
+ async fetchAndStoreList(filter) {
3002
+ const items = await this.fetchListNetwork(filter);
3003
+ this.cache.setList(filter, items);
3004
+ return items;
3111
3005
  }
3112
- if (typeof value === "string") {
3113
- const parsed = Number(value);
3114
- if (!Number.isNaN(parsed)) {
3115
- return { valid: true, errors: [], value: parsed };
3006
+ /** Raw network call: GET /trpc/catalog.get?input=<superjson> */
3007
+ async fetchResolveNetwork(ref) {
3008
+ const url = trpcGetUrl(this.baseUrl, "catalog.get", { ref });
3009
+ const res = await this.httpClient(url, {
3010
+ method: "GET",
3011
+ headers: { Accept: "application/json" }
3012
+ });
3013
+ if (!res.ok) {
3014
+ throw new CatalogHttpError(
3015
+ `catalog.get ${ref} failed: ${res.status} ${res.statusText}`,
3016
+ res.status,
3017
+ url
3018
+ );
3116
3019
  }
3020
+ const data = await parseTrpcGet(res);
3021
+ if (data === null || data === void 0) return null;
3022
+ return normalizeCatalogAsset(data);
3117
3023
  }
3118
- return { valid: false, errors: [`Placeholder "${key}" must be a valid number`] };
3119
- }
3120
- function validateBoolean(key, value) {
3121
- if (typeof value === "boolean") {
3122
- return { valid: true, errors: [], value };
3123
- }
3124
- if (typeof value === "string") {
3125
- const lower = value.toLowerCase();
3126
- if (lower === "true" || lower === "1" || lower === "yes") {
3127
- return { valid: true, errors: [], value: true };
3024
+ /** Raw network call: GET /trpc/catalog.list?input=<superjson> */
3025
+ async fetchListNetwork(filter) {
3026
+ const input = filter ? { filter } : {};
3027
+ const url = trpcGetUrl(this.baseUrl, "catalog.list", input);
3028
+ const res = await this.httpClient(url, {
3029
+ method: "GET",
3030
+ headers: { Accept: "application/json" }
3031
+ });
3032
+ if (!res.ok) {
3033
+ throw new CatalogHttpError(
3034
+ `catalog.list failed: ${res.status} ${res.statusText}`,
3035
+ res.status,
3036
+ url
3037
+ );
3128
3038
  }
3129
- if (lower === "false" || lower === "0" || lower === "no") {
3130
- return { valid: true, errors: [], value: false };
3039
+ const data = await parseTrpcGet(res);
3040
+ if (!Array.isArray(data)) {
3041
+ throw new CatalogHttpError(
3042
+ `catalog.list returned non-array payload (got ${typeof data})`,
3043
+ 200,
3044
+ url
3045
+ );
3131
3046
  }
3047
+ return data.map(normalizeCatalogAsset);
3132
3048
  }
3133
- return { valid: false, errors: [`Placeholder "${key}" must be a boolean`] };
3049
+ };
3050
+ function trpcGetUrl(baseUrl, procedure, input) {
3051
+ const wrapped = JSON.stringify({ json: input });
3052
+ return `${baseUrl.replace(/\/+$/, "")}/trpc/${procedure}?input=${encodeURIComponent(wrapped)}`;
3134
3053
  }
3135
- function validateEnum(key, value, allowedValues) {
3136
- if (!allowedValues || allowedValues.length === 0) {
3137
- return { valid: false, errors: [`Placeholder "${key}" (enum) has no allowed values defined`] };
3054
+ async function parseTrpcGet(res) {
3055
+ const text2 = await res.text();
3056
+ let body;
3057
+ try {
3058
+ body = text2.length > 0 ? JSON.parse(text2) : null;
3059
+ } catch {
3060
+ throw new CatalogHttpError(
3061
+ `tRPC response is not valid JSON: ${text2.slice(0, 200)}`,
3062
+ res.status
3063
+ );
3138
3064
  }
3139
- const stringValue = String(value);
3140
- const matches = allowedValues.some((v) => String(v) === stringValue);
3141
- if (!matches) {
3142
- return {
3143
- valid: false,
3144
- errors: [
3145
- `Placeholder "${key}" value "${stringValue}" is not in allowed values: [${allowedValues.join(", ")}]`
3146
- ]
3147
- };
3065
+ if (body && typeof body === "object" && "error" in body) {
3066
+ const err = body.error;
3067
+ throw new CatalogHttpError(
3068
+ `tRPC error: ${err.message ?? "unknown"}${err.data?.code ? ` (${err.data.code})` : ""}`,
3069
+ res.status
3070
+ );
3148
3071
  }
3149
- const matched = allowedValues.find((v) => String(v) === stringValue);
3150
- return { valid: true, errors: [], value: matched };
3151
- }
3152
- function validateOauth(key, value) {
3153
- if (typeof value !== "string") {
3154
- return { valid: false, errors: [`Placeholder "${key}" (oauth) must be a string`] };
3072
+ const result = body?.result;
3073
+ if (!result || !("data" in result)) {
3074
+ throw new CatalogHttpError(`tRPC response missing result.data envelope`, res.status);
3155
3075
  }
3156
- if (value.length === 0) {
3157
- return { valid: false, errors: [`Placeholder "${key}" (oauth) cannot be empty`] };
3076
+ const data = result.data;
3077
+ if (data && typeof data === "object" && !Array.isArray(data) && "json" in data) {
3078
+ return data.json;
3158
3079
  }
3159
- return { valid: true, errors: [], value };
3080
+ return data;
3160
3081
  }
3161
- function validateInstancePicker(key, value) {
3162
- if (typeof value !== "string") {
3163
- return {
3164
- valid: false,
3165
- errors: [`Placeholder "${key}" (instance-picker) must be a string UUID`]
3166
- };
3167
- }
3168
- const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
3169
- if (!uuidPattern.test(value)) {
3170
- return {
3171
- valid: false,
3172
- errors: [`Placeholder "${key}" (instance-picker) must be a valid UUID`]
3173
- };
3082
+ function normalizeCatalogAsset(raw) {
3083
+ if (!raw || typeof raw !== "object") {
3084
+ throw new CatalogHttpError("Catalog response item is not an object", 200);
3174
3085
  }
3175
- return { valid: true, errors: [], value };
3086
+ const r = raw;
3087
+ const publisher = pickString(r, "publisher") ?? pickString(r, "publisherSlug") ?? "";
3088
+ const name = pickString(r, "name") ?? "";
3089
+ const version = pickString(r, "version") ?? "";
3090
+ const id = pickString(r, "id") ?? `${publisher}/${name}@${version}`;
3091
+ const kind = pickString(r, "kind") ?? "";
3092
+ const manifest = typeof r.manifest === "object" && r.manifest !== null ? r.manifest : typeof r.manifestJson === "object" && r.manifestJson !== null ? r.manifestJson : {};
3093
+ const asset = {
3094
+ id,
3095
+ kind,
3096
+ publisher,
3097
+ name,
3098
+ version,
3099
+ manifest
3100
+ };
3101
+ const sha = pickString(r, "sha256") ?? pickString(r, "tarballSha256");
3102
+ if (sha) asset.sha256 = sha;
3103
+ const description = pickString(r, "description");
3104
+ if (description) asset.description = description;
3105
+ const license = pickString(r, "license");
3106
+ if (license) asset.license = license;
3107
+ const category = pickString(r, "category");
3108
+ if (category) asset.category = category;
3109
+ const repo = pickString(r, "repo");
3110
+ if (repo) asset.repo = repo;
3111
+ const domainSlug = pickString(r, "domainSlug");
3112
+ if (domainSlug) asset.domainSlug = domainSlug;
3113
+ return asset;
3176
3114
  }
3177
-
3178
- // library/src/preset/interpolate.ts
3179
- var InterpolationError = class extends Error {
3180
- constructor(message) {
3181
- super(message);
3182
- this.name = "InterpolationError";
3115
+ function pickString(r, key) {
3116
+ const v = r[key];
3117
+ return typeof v === "string" ? v : void 0;
3118
+ }
3119
+ var CATALOG_REQUEST_TIMEOUT_MS = 1e4;
3120
+ var defaultFetch = async (url, init) => {
3121
+ if (typeof fetch === "undefined") {
3122
+ throw new Error("global fetch is not available; pass an httpClient explicitly");
3183
3123
  }
3124
+ const requestInit = init ?? {};
3125
+ return fetch(url, {
3126
+ ...requestInit,
3127
+ signal: requestInit.signal ?? AbortSignal.timeout(CATALOG_REQUEST_TIMEOUT_MS)
3128
+ });
3184
3129
  };
3185
- function interpolate(materialize, baseConfig, context, credentialRefPrefix) {
3186
- const config = { ...baseConfig ?? {} };
3187
- const secretsToStore = [];
3188
- let credentialRef;
3189
- if (!materialize) {
3190
- for (const [key, value] of Object.entries(context.inputs)) {
3191
- config[key] = value;
3130
+
3131
+ // library/src/remote/rest-catalog-source.ts
3132
+ var DEFAULT_TTL_MS2 = 864e5;
3133
+ var RestCatalogSource = class {
3134
+ id;
3135
+ baseUrl;
3136
+ cache;
3137
+ cacheTtlMs;
3138
+ httpClient;
3139
+ silentBackgroundErrors;
3140
+ /** Tracks in-flight background revalidations so callers can `await` them in tests. */
3141
+ inFlight = /* @__PURE__ */ new Map();
3142
+ constructor(opts) {
3143
+ this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
3144
+ this.id = `rest:${this.baseUrl}`;
3145
+ this.cacheTtlMs = opts.cacheTtlMs ?? DEFAULT_TTL_MS2;
3146
+ this.httpClient = opts.httpClient ?? defaultFetch2;
3147
+ this.silentBackgroundErrors = opts.silentBackgroundErrors ?? false;
3148
+ this.cache = opts.cache ?? new CatalogCache({
3149
+ dir: opts.cacheDir,
3150
+ ttlMs: this.cacheTtlMs,
3151
+ now: opts.now
3152
+ });
3153
+ }
3154
+ // ── ICatalogSource ──────────────────────────────────────────────────────
3155
+ /**
3156
+ * Resolve a single asset by canonical ref (`<publisher>/<name>@<version>`).
3157
+ *
3158
+ * Cache: hit returns immediately. Miss fetches `/api/catalog/get?ref=<ref>`. Stale
3159
+ * entry serves the cached value and triggers a background refresh.
3160
+ *
3161
+ * Air-gapped: throws {@link OfflineError} on cache miss.
3162
+ */
3163
+ async resolve(ref) {
3164
+ const fresh = this.cache.getResolve(ref);
3165
+ if (fresh) return fresh.value;
3166
+ if (this.cache.airGapped) {
3167
+ const stale2 = this.cache.getResolveStale(ref);
3168
+ if (stale2) return stale2.value;
3169
+ throw new OfflineError(`Asset '${ref}' not in cache`);
3192
3170
  }
3193
- if (Object.keys(context.secrets).length > 0) {
3194
- credentialRef = `${credentialRefPrefix}/creds`;
3195
- for (const [key, value] of Object.entries(context.secrets)) {
3196
- secretsToStore.push({ ref: `${credentialRef}/${key}`, value });
3197
- }
3171
+ const stale = this.cache.getResolveStale(ref);
3172
+ if (stale) {
3173
+ this.scheduleBackground(`resolve:${ref}`, () => this.fetchAndStoreResolve(ref));
3174
+ return stale.value;
3198
3175
  }
3199
- return { config, credentialRef, secretsToStore };
3176
+ return this.fetchAndStoreResolve(ref);
3200
3177
  }
3201
- for (const [configPath, template] of Object.entries(materialize)) {
3202
- const resolved = resolveTemplate(template, context);
3203
- if (resolved.hasSecret) {
3204
- if (!credentialRef) {
3205
- credentialRef = `${credentialRefPrefix}/creds`;
3206
- }
3207
- const secretRef = `${credentialRef}/${configPath}`;
3208
- secretsToStore.push({ ref: secretRef, value: resolved.value });
3209
- setNestedValue(config, configPath, `secret://${secretRef}`);
3210
- } else {
3211
- setNestedValue(config, configPath, resolved.value);
3178
+ /**
3179
+ * List assets matching `filter`.
3180
+ *
3181
+ * Cache: hit returns immediately. Miss fetches `/api/catalog/list`. Stale
3182
+ * entry serves the cached array and triggers a background refresh.
3183
+ *
3184
+ * Air-gapped: throws {@link OfflineError} on cache miss.
3185
+ *
3186
+ * Note: `filter.prefix` is not supported by the forge-store REST API and is silently ignored.
3187
+ */
3188
+ async listAssets(filter) {
3189
+ const fresh = this.cache.getList(filter);
3190
+ if (fresh) return fresh.items;
3191
+ if (this.cache.airGapped) {
3192
+ const stale2 = this.cache.getListStale(filter);
3193
+ if (stale2) return stale2.items;
3194
+ throw new OfflineError("Catalog list not in cache");
3212
3195
  }
3213
- }
3214
- return { config, credentialRef, secretsToStore };
3215
- }
3216
- async function storeSecrets(router, secretsToStore) {
3217
- for (const { ref, value } of secretsToStore) {
3218
- const encoded = new TextEncoder().encode(value);
3219
- await router.setSecret(ref, encoded);
3220
- }
3221
- }
3222
- function hasInterpolationTokens(template) {
3223
- return INTERPOLATION_PATTERN.test(template);
3224
- }
3225
- function extractReferencedKeys(materialize) {
3226
- const inputKeys = /* @__PURE__ */ new Set();
3227
- const secretKeys = /* @__PURE__ */ new Set();
3228
- for (const template of Object.values(materialize)) {
3229
- const pattern = new RegExp(INTERPOLATION_PATTERN.source, "g");
3230
- for (const match of template.matchAll(pattern)) {
3231
- const namespace = match[1];
3232
- const key = match[2];
3233
- if (namespace === "INPUT") inputKeys.add(key);
3234
- else if (namespace === "SECRET") secretKeys.add(key);
3196
+ const stale = this.cache.getListStale(filter);
3197
+ if (stale) {
3198
+ const filterKey2 = JSON.stringify(filter ?? null);
3199
+ this.scheduleBackground(`list:${filterKey2}`, () => this.fetchAndStoreList(filter));
3200
+ return stale.items;
3235
3201
  }
3236
- }
3237
- return { inputKeys: [...inputKeys], secretKeys: [...secretKeys] };
3238
- }
3239
- var INTERPOLATION_PATTERN = /\$\{(INPUT|SECRET):([^}]+)\}/;
3240
- function resolveTemplate(template, context) {
3241
- const singleMatch = template.match(/^\$\{(INPUT|SECRET):([^}]+)\}$/);
3242
- if (singleMatch) {
3243
- const [, namespace, key] = singleMatch;
3244
- if (namespace === "SECRET") {
3245
- if (!(key in context.secrets)) {
3246
- throw new InterpolationError(`Missing secret key "${key}" in interpolation context`);
3202
+ return this.fetchAndStoreList(filter);
3203
+ }
3204
+ /**
3205
+ * Fetch the content-addressed tarball for an asset.
3206
+ *
3207
+ * Steps:
3208
+ * 1. `GET /api/tarball/manifest?sha256=<sha256>` → JSON with `{ url, ... }`
3209
+ * 2. `GET <manifest.url>` following redirects → raw bytes
3210
+ * 3. Verify SHA256 client-side — hard fail on mismatch
3211
+ * 4. Write to cache on success
3212
+ *
3213
+ * Cache: tarballs are content-addressed and cached forever once written.
3214
+ *
3215
+ * Air-gapped: throws {@link OfflineError} on cache miss.
3216
+ *
3217
+ * @throws {@link TarballHashMismatchError} when the downloaded SHA256 does not match.
3218
+ */
3219
+ async fetchTarball(_ref, sha256) {
3220
+ const cached = this.cache.readTarball(sha256);
3221
+ if (cached) {
3222
+ const actual2 = sha256Hex(cached);
3223
+ if (actual2 !== sha256) {
3224
+ throw new TarballHashMismatchError(sha256, actual2);
3247
3225
  }
3248
- return { value: context.secrets[key], hasSecret: true };
3226
+ return cached;
3249
3227
  }
3250
- if (!(key in context.inputs)) {
3251
- throw new InterpolationError(`Missing input key "${key}" in interpolation context`);
3228
+ if (this.cache.airGapped) {
3229
+ throw new OfflineError(`Tarball '${sha256}' not in cache`);
3252
3230
  }
3253
- return { value: context.inputs[key], hasSecret: false };
3254
- }
3255
- const tokens = [...template.matchAll(/\$\{(INPUT|SECRET):([^}]+)\}/g)];
3256
- const hasSecretToken = tokens.some(([, ns]) => ns === "SECRET");
3257
- if (hasSecretToken) {
3258
- throw new InterpolationError(
3259
- `Template "${template}" mixes \${SECRET:...} with other tokens. SECRET must be the sole token in a template value.`
3260
- );
3261
- }
3262
- const result = template.replace(/\$\{INPUT:([^}]+)\}/g, (_, key) => {
3263
- if (!(key in context.inputs)) {
3264
- throw new InterpolationError(`Missing input key "${key}" in interpolation context`);
3231
+ const manifestUrl = `${this.baseUrl}/api/tarball/manifest?sha256=${encodeURIComponent(sha256)}`;
3232
+ const manifestRes = await this.httpClient(manifestUrl, {
3233
+ method: "GET",
3234
+ headers: { Accept: "application/json" }
3235
+ });
3236
+ if (!manifestRes.ok) {
3237
+ throw new CatalogHttpError(
3238
+ `Failed to fetch tarball manifest for ${sha256}: ${manifestRes.status} ${manifestRes.statusText}`,
3239
+ manifestRes.status,
3240
+ manifestUrl
3241
+ );
3265
3242
  }
3266
- return String(context.inputs[key] ?? "");
3267
- });
3268
- return { value: result, hasSecret: false };
3269
- }
3270
- function setNestedValue(obj, path13, value) {
3271
- const parts = path13.split(".");
3272
- let current = obj;
3273
- for (let i = 0; i < parts.length - 1; i++) {
3274
- const key = parts[i];
3275
- if (current[key] === void 0 || current[key] === null || typeof current[key] !== "object") {
3276
- current[key] = {};
3243
+ const manifestText = await manifestRes.text();
3244
+ let manifest;
3245
+ try {
3246
+ manifest = JSON.parse(manifestText);
3247
+ } catch {
3248
+ throw new CatalogHttpError(
3249
+ `Tarball manifest response is not valid JSON: ${manifestText.slice(0, 200)}`,
3250
+ manifestRes.status,
3251
+ manifestUrl
3252
+ );
3277
3253
  }
3278
- current = current[key];
3254
+ const downloadUrl = manifest && typeof manifest === "object" && "url" in manifest ? manifest.url : void 0;
3255
+ if (typeof downloadUrl !== "string") {
3256
+ throw new CatalogHttpError(
3257
+ `Tarball manifest missing 'url' field`,
3258
+ manifestRes.status,
3259
+ manifestUrl
3260
+ );
3261
+ }
3262
+ const tarballRes = await this.httpClient(downloadUrl, { method: "GET", redirect: "follow" });
3263
+ if (!tarballRes.ok) {
3264
+ throw new CatalogHttpError(
3265
+ `Failed to fetch tarball ${sha256}: ${tarballRes.status} ${tarballRes.statusText}`,
3266
+ tarballRes.status,
3267
+ downloadUrl
3268
+ );
3269
+ }
3270
+ const buf = new Uint8Array(await tarballRes.arrayBuffer());
3271
+ const actual = sha256Hex(buf);
3272
+ if (actual !== sha256) {
3273
+ throw new TarballHashMismatchError(sha256, actual);
3274
+ }
3275
+ this.cache.writeTarball(sha256, buf);
3276
+ return buf;
3279
3277
  }
3280
- current[parts[parts.length - 1]] = value;
3281
- }
3282
-
3283
- // library/src/preset/apply.ts
3284
- async function applyPreset(presetRef, manifest, inputs, opts) {
3285
- const depth = opts._depth ?? 0;
3286
- const pinPolicy = opts.pinPolicy ?? "minor-track";
3287
- const depthError = validateNestingDepth(presetRef, depth);
3288
- if (depthError) {
3289
- return { instances: [], assignments: [], errors: [depthError] };
3278
+ /**
3279
+ * Enumerate available versions for a version-less ref. The forge-store REST
3280
+ * API has no version-list route, so this derives versions from `listAssets`
3281
+ * (filtered to the publisher) and keeps only the matching kind+name. Reuses
3282
+ * the cached list path, so repeated calls are cheap.
3283
+ */
3284
+ async listVersions(ref) {
3285
+ const parsed = parseAssetRef(ref);
3286
+ const wantPublisher = parsed.publisher?.replace(/^@/, "");
3287
+ const assets = await this.listAssets(wantPublisher ? { publisher: wantPublisher } : void 0);
3288
+ return assets.filter(
3289
+ (a) => a.kind === parsed.kind && a.name === parsed.name && (wantPublisher === void 0 || a.publisher.replace(/^@/, "") === wantPublisher)
3290
+ ).map((a) => a.version);
3290
3291
  }
3291
- const result = { instances: [], assignments: [], errors: [] };
3292
- const itemResults = /* @__PURE__ */ new Map();
3293
- for (const item of manifest.items) {
3294
- const itemResult = await applyItemImpl(
3295
- presetRef,
3296
- item,
3297
- inputs,
3298
- opts,
3299
- depth,
3300
- pinPolicy,
3301
- itemResults
3302
- );
3303
- result.errors.push(...itemResult.errors);
3304
- result.instances.push(...itemResult.instances);
3305
- if (item.id && itemResult.instances.length > 0) {
3306
- itemResults.set(item.id, itemResult.instances[0]);
3292
+ /**
3293
+ * Single synthetic Source pointing at the configured forge-store URL.
3294
+ */
3295
+ async listSources() {
3296
+ return [{ id: this.id, type: "rest-catalog", url: this.baseUrl }];
3297
+ }
3298
+ // ── extension API ───────────────────────────────────────────────────────
3299
+ /**
3300
+ * Force a metadata-cache refresh. Wipes all resolve/list entries and
3301
+ * re-fetches `listAssets()` (no filter) to pre-warm the canonical query.
3302
+ *
3303
+ * Driven by `skaile update`. Tarballs (content-addressed) are not touched.
3304
+ *
3305
+ * In air-gapped mode this method **does** make a network call (bypassing
3306
+ * the TTL=0 gate); it is the only way to repopulate the cache.
3307
+ */
3308
+ async refresh() {
3309
+ this.cache.invalidateAll();
3310
+ const items = await this.fetchListNetwork(void 0);
3311
+ this.cache.setList(void 0, items);
3312
+ for (const item of items) {
3313
+ this.cache.setResolve(item.id, item);
3307
3314
  }
3308
- result.assignments.push(...itemResult.assignments);
3315
+ return { assetsCached: items.length };
3309
3316
  }
3310
- return result;
3311
- }
3312
- async function applyPresetFromData(presetRef, data, inputs, opts) {
3313
- const parseResult = parsePreset(data);
3314
- if (!parseResult.ok || !parseResult.preset) {
3315
- return {
3316
- instances: [],
3317
- assignments: [],
3318
- errors: parseResult.errors.map((e) => `Parse: ${e}`)
3319
- };
3317
+ /**
3318
+ * Wait for any in-flight stale-while-revalidate background fetches to settle.
3319
+ * Test-only escape hatch.
3320
+ */
3321
+ async waitForBackground() {
3322
+ const promises = Array.from(this.inFlight.values());
3323
+ await Promise.allSettled(promises);
3320
3324
  }
3321
- return applyPreset(presetRef, parseResult.preset, inputs, opts);
3322
- }
3323
- async function applyItemImpl(presetRef, item, inputs, opts, depth, pinPolicy, itemResults) {
3324
- const { library } = opts;
3325
- const resolved = await resolveItem(item, library, opts.knownPresetRefs);
3326
- switch (resolved.kind) {
3327
- case "cross-item": {
3328
- const target = itemResults.get(resolved.targetId);
3329
- if (!target) {
3330
- return {
3331
- instances: [],
3332
- assignments: [],
3333
- errors: [
3334
- `Cross-item ref "#${resolved.targetId}" not yet resolved (items must be ordered)`
3335
- ]
3336
- };
3337
- }
3338
- return { instances: [target], assignments: [], errors: [] };
3339
- }
3340
- case "library-instance": {
3341
- if (!resolved.instance) {
3342
- return {
3343
- instances: [],
3344
- assignments: [],
3345
- errors: [`Library instance "${resolved.instanceId}" not found`]
3346
- };
3347
- }
3325
+ // ── private ─────────────────────────────────────────────────────────────
3326
+ scheduleBackground(key, fn) {
3327
+ if (this.inFlight.has(key)) return;
3328
+ const p = (async () => {
3348
3329
  try {
3349
- const asgn = await library.assign(opts.workspaceId, resolved.instanceId, pinPolicy);
3350
- return { instances: [resolved.instance], assignments: [asgn], errors: [] };
3351
- } catch {
3352
- return { instances: [resolved.instance], assignments: [], errors: [] };
3353
- }
3354
- }
3355
- case "nested-preset": {
3356
- if (!opts.loadPreset) {
3357
- return {
3358
- instances: [],
3359
- assignments: [],
3360
- errors: [`Cannot resolve nested preset "${resolved.presetRef}" \u2014 no loader provided`]
3361
- };
3362
- }
3363
- const nestedData = await opts.loadPreset(resolved.presetRef);
3364
- if (!nestedData) {
3365
- return {
3366
- instances: [],
3367
- assignments: [],
3368
- errors: [`Nested preset "${resolved.presetRef}" not found`]
3369
- };
3370
- }
3371
- const parseResult = parsePreset(nestedData);
3372
- if (!parseResult.ok || !parseResult.preset) {
3373
- return { instances: [], assignments: [], errors: parseResult.errors };
3374
- }
3375
- const nestedInputs = {
3376
- ...item.inputs ?? {},
3377
- ...inputs
3378
- };
3379
- const nestedResult = await applyPreset(resolved.presetRef, parseResult.preset, nestedInputs, {
3380
- ...opts,
3381
- _depth: depth + 1
3382
- });
3383
- return {
3384
- instances: nestedResult.instances,
3385
- assignments: nestedResult.assignments,
3386
- errors: nestedResult.errors
3387
- };
3388
- }
3389
- case "asset": {
3390
- const itemInputs = { ...inputs };
3391
- let validatedValues = {};
3392
- const secretValues = {};
3393
- if (item.placeholders && item.placeholders.length > 0) {
3394
- const nonSecretPhs = item.placeholders.filter((ph) => !isSecretPlaceholder(ph));
3395
- const secretPhs = item.placeholders.filter((ph) => isSecretPlaceholder(ph));
3396
- const validation = validateAllPlaceholders(nonSecretPhs, itemInputs);
3397
- if (!validation.valid) {
3398
- return { instances: [], assignments: [], errors: validation.errors };
3399
- }
3400
- validatedValues = validation.values;
3401
- const secretValidation = validateAllPlaceholders(secretPhs, itemInputs);
3402
- if (!secretValidation.valid) {
3403
- return { instances: [], assignments: [], errors: secretValidation.errors };
3404
- }
3405
- for (const [key, value] of Object.entries(secretValidation.values)) {
3406
- secretValues[key] = String(value);
3330
+ await fn();
3331
+ } catch (err) {
3332
+ if (!this.silentBackgroundErrors) {
3333
+ process.stderr.write(
3334
+ `[skaile/catalog] Background refresh failed for ${key}: ${err instanceof Error ? err.message : String(err)}
3335
+ `
3336
+ );
3407
3337
  }
3338
+ } finally {
3339
+ this.inFlight.delete(key);
3408
3340
  }
3409
- const credentialRefPrefix = `preset/${presetRef}/${item.id ?? resolved.defRef}`;
3410
- const context = {
3411
- inputs: validatedValues,
3412
- secrets: secretValues
3413
- };
3414
- const interpolated = interpolate(
3415
- item.materialize,
3416
- item.config,
3417
- context,
3418
- credentialRefPrefix
3341
+ })();
3342
+ this.inFlight.set(key, p);
3343
+ }
3344
+ async fetchAndStoreResolve(ref) {
3345
+ const value = await this.fetchResolveNetwork(ref);
3346
+ this.cache.setResolve(ref, value);
3347
+ return value;
3348
+ }
3349
+ async fetchAndStoreList(filter) {
3350
+ const items = await this.fetchListNetwork(filter);
3351
+ this.cache.setList(filter, items);
3352
+ return items;
3353
+ }
3354
+ /** Raw network call: GET /api/catalog/get?ref=<ref> */
3355
+ async fetchResolveNetwork(ref) {
3356
+ const url = `${this.baseUrl}/api/catalog/get?ref=${encodeURIComponent(ref)}`;
3357
+ const res = await this.httpClient(url, {
3358
+ method: "GET",
3359
+ headers: { Accept: "application/json" }
3360
+ });
3361
+ if (!res.ok) {
3362
+ throw new CatalogHttpError(
3363
+ `catalog/get ${ref} failed: ${res.status} ${res.statusText}`,
3364
+ res.status,
3365
+ url
3366
+ );
3367
+ }
3368
+ const data = await parseRestResponse(res, url);
3369
+ if (data === null || data === void 0) return null;
3370
+ return normalizeRestAsset(data);
3371
+ }
3372
+ /** Raw network call: GET /api/catalog/list?publisher=&kind=&category= */
3373
+ async fetchListNetwork(filter) {
3374
+ const params = new URLSearchParams();
3375
+ if (filter?.publisher) params.set("publisher", filter.publisher);
3376
+ if (filter?.kind) params.set("kind", filter.kind);
3377
+ const qs = params.toString();
3378
+ const url = `${this.baseUrl}/api/catalog/list${qs ? `?${qs}` : ""}`;
3379
+ const res = await this.httpClient(url, {
3380
+ method: "GET",
3381
+ headers: { Accept: "application/json" }
3382
+ });
3383
+ if (!res.ok) {
3384
+ throw new CatalogHttpError(
3385
+ `catalog/list failed: ${res.status} ${res.statusText}`,
3386
+ res.status,
3387
+ url
3419
3388
  );
3420
- if (opts.secretsRouter && interpolated.secretsToStore.length > 0) {
3421
- try {
3422
- await storeSecrets(opts.secretsRouter, interpolated.secretsToStore);
3423
- } catch (err) {
3424
- return {
3425
- instances: [],
3426
- assignments: [],
3427
- errors: [
3428
- `Secret storage failed for "${item.ref}": ${err instanceof Error ? err.message : String(err)}`
3429
- ]
3430
- };
3431
- }
3432
- }
3433
- try {
3434
- const instance = await library.createInstance({
3435
- defRef: resolved.defRef,
3436
- defPin: pinPolicy,
3437
- config: interpolated.config,
3438
- credentialRef: interpolated.credentialRef,
3439
- createdBy: opts.createdBy ?? `preset:${presetRef}`
3440
- });
3441
- const assignments2 = [];
3442
- try {
3443
- const asgn = await library.assign(opts.workspaceId, instance.id, pinPolicy);
3444
- assignments2.push(asgn);
3445
- } catch {
3446
- }
3447
- return { instances: [instance], assignments: assignments2, errors: [] };
3448
- } catch (err) {
3449
- return {
3450
- instances: [],
3451
- assignments: [],
3452
- errors: [
3453
- `Instance creation failed for "${item.ref}": ${err instanceof Error ? err.message : String(err)}`
3454
- ]
3455
- };
3456
- }
3457
3389
  }
3458
- }
3459
- }
3460
-
3461
- // library/src/preset/upgrade.ts
3462
- function detectChanges(oldManifest, newManifest) {
3463
- const oldItems = indexItems(oldManifest.items);
3464
- const newItems = indexItems(newManifest.items);
3465
- const addedItems = [];
3466
- const removedItems = [];
3467
- const changedRefItems = [];
3468
- const configChangedItems = [];
3469
- for (const [key, oldItem] of oldItems) {
3470
- if (!newItems.has(key)) {
3471
- removedItems.push(oldItem);
3390
+ const data = await parseRestResponse(res, url);
3391
+ if (!Array.isArray(data)) {
3392
+ throw new CatalogHttpError(
3393
+ `catalog/list returned non-array payload (got ${typeof data})`,
3394
+ 200,
3395
+ url
3396
+ );
3472
3397
  }
3398
+ return data.map(normalizeRestAsset);
3473
3399
  }
3474
- for (const [key, newItem] of newItems) {
3475
- if (!oldItems.has(key)) {
3476
- addedItems.push(newItem);
3477
- }
3400
+ };
3401
+ async function parseRestResponse(res, url) {
3402
+ const text2 = await res.text();
3403
+ if (text2.length === 0) return null;
3404
+ try {
3405
+ return JSON.parse(text2);
3406
+ } catch {
3407
+ throw new CatalogHttpError(
3408
+ `REST response is not valid JSON: ${text2.slice(0, 200)}`,
3409
+ res.status,
3410
+ url
3411
+ );
3478
3412
  }
3479
- for (const [key, newItem] of newItems) {
3480
- const oldItem = oldItems.get(key);
3481
- if (!oldItem) continue;
3482
- if (oldItem.ref !== newItem.ref) {
3483
- changedRefItems.push({ old: oldItem, new: newItem });
3484
- } else if (!deepEqual(oldItem.placeholders, newItem.placeholders) || !deepEqual(oldItem.materialize, newItem.materialize)) {
3485
- changedRefItems.push({ old: oldItem, new: newItem });
3486
- } else if (!deepEqual(oldItem.config, newItem.config)) {
3487
- configChangedItems.push({ old: oldItem, new: newItem });
3488
- }
3413
+ }
3414
+ function normalizeRestAsset(raw) {
3415
+ if (!raw || typeof raw !== "object") {
3416
+ throw new CatalogHttpError("Catalog REST response item is not an object", 200);
3489
3417
  }
3490
- const isStructural = addedItems.length > 0 || removedItems.length > 0 || changedRefItems.length > 0;
3491
- const kind = isStructural ? "structural" : "non-structural";
3492
- const summaryParts = [];
3493
- if (addedItems.length > 0) summaryParts.push(`+${addedItems.length} items`);
3494
- if (removedItems.length > 0) summaryParts.push(`-${removedItems.length} items`);
3495
- if (changedRefItems.length > 0) summaryParts.push(`${changedRefItems.length} ref changes`);
3496
- if (configChangedItems.length > 0)
3497
- summaryParts.push(`${configChangedItems.length} config changes`);
3498
- const summary = summaryParts.length > 0 ? `${kind}: ${summaryParts.join(", ")}` : "no changes";
3499
- return {
3418
+ const r = raw;
3419
+ const publisher = pickString2(r, "publisherNamespace") ?? pickString2(r, "publisher") ?? "";
3420
+ const name = pickString2(r, "name") ?? "";
3421
+ const version = pickString2(r, "version") ?? "";
3422
+ const kind = pickString2(r, "kind") ?? "";
3423
+ const id = `${publisher}/${name}@${version}`;
3424
+ const requiresParsed = tryParseJson(pickString2(r, "requiresJson"));
3425
+ const filesParsed = tryParseJson(pickString2(r, "filesJson"));
3426
+ const manifest = { ...requiresParsed, ...filesParsed };
3427
+ const asset = {
3428
+ id,
3500
3429
  kind,
3501
- addedItems,
3502
- removedItems,
3503
- changedRefItems,
3504
- configChangedItems,
3505
- summary
3430
+ publisher,
3431
+ name,
3432
+ version,
3433
+ manifest
3506
3434
  };
3435
+ const sha = pickString2(r, "sha256");
3436
+ if (sha) asset.sha256 = sha;
3437
+ const description = pickString2(r, "description");
3438
+ if (description) asset.description = description;
3439
+ const license = pickString2(r, "license");
3440
+ if (license) asset.license = license;
3441
+ const category = pickString2(r, "category");
3442
+ if (category) asset.category = category;
3443
+ const domainSlug = pickString2(r, "domainSlug");
3444
+ if (domainSlug) asset.domainSlug = domainSlug;
3445
+ return asset;
3507
3446
  }
3508
- function evaluateUpgrade(oldManifest, newManifest) {
3509
- const diff = detectChanges(oldManifest, newManifest);
3510
- if (diff.kind === "non-structural") {
3511
- return { autoUpgradable: true, diff };
3512
- }
3513
- const requiredActions = [];
3514
- if (diff.addedItems.length > 0) {
3515
- requiredActions.push(
3516
- `Create ${diff.addedItems.length} new instance(s): ${diff.addedItems.map((i) => i.ref).join(", ")}`
3517
- );
3518
- }
3519
- if (diff.removedItems.length > 0) {
3520
- requiredActions.push(
3521
- `Remove ${diff.removedItems.length} instance(s): ${diff.removedItems.map((i) => i.ref).join(", ")}`
3522
- );
3523
- }
3524
- if (diff.changedRefItems.length > 0) {
3525
- requiredActions.push(`Replace ${diff.changedRefItems.length} instance(s) with new refs`);
3447
+ function pickString2(r, key) {
3448
+ const v = r[key];
3449
+ return typeof v === "string" ? v : void 0;
3450
+ }
3451
+ function tryParseJson(s) {
3452
+ if (!s) return {};
3453
+ try {
3454
+ const parsed = JSON.parse(s);
3455
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
3456
+ return parsed;
3457
+ }
3458
+ } catch {
3526
3459
  }
3527
- return { autoUpgradable: false, diff, requiredActions };
3460
+ return {};
3528
3461
  }
3529
- async function applyNonStructuralUpgrade(newManifest, existingInstances, library) {
3530
- let updated = 0;
3531
- const errors = [];
3532
- for (const item of newManifest.items) {
3533
- const key = itemKey(item);
3534
- const instanceId = existingInstances.get(key);
3535
- if (!instanceId) continue;
3536
- if (item.config) {
3537
- try {
3538
- await library.updateInstance(instanceId, {
3539
- config: item.config
3540
- });
3541
- updated++;
3542
- } catch (err) {
3543
- errors.push(
3544
- `Failed to update instance "${instanceId}": ${err instanceof Error ? err.message : String(err)}`
3462
+ var CATALOG_REQUEST_TIMEOUT_MS2 = 1e4;
3463
+ var defaultFetch2 = async (url, init) => {
3464
+ if (typeof fetch === "undefined") {
3465
+ throw new Error("global fetch is not available; pass an httpClient explicitly");
3466
+ }
3467
+ const requestInit = init ?? {};
3468
+ return fetch(url, {
3469
+ ...requestInit,
3470
+ signal: requestInit.signal ?? AbortSignal.timeout(CATALOG_REQUEST_TIMEOUT_MS2)
3471
+ });
3472
+ };
3473
+ var MANIFEST_FILENAME = "skaile.manifest.yaml";
3474
+ async function writeManifestIfMissing(input) {
3475
+ const manifestPath = path5.join(input.libraryPath, MANIFEST_FILENAME);
3476
+ if (fs10.existsSync(manifestPath)) return { wrote: false, manifestPath };
3477
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3478
+ const yaml = `# Generated by skaile library add on ${now} (structure: ${input.structure})
3479
+ # Edit by hand to inline discovered assets; or run \`skaile library refresh-manifest\`.
3480
+ publisher: ""
3481
+ assets: []
3482
+ `;
3483
+ fs10.writeFileSync(manifestPath, yaml, "utf-8");
3484
+ if (input.ownership !== "owner") {
3485
+ const excludePath = path5.join(input.libraryPath, ".git", "info", "exclude");
3486
+ if (fs10.existsSync(path5.dirname(excludePath))) {
3487
+ const current = fs10.existsSync(excludePath) ? fs10.readFileSync(excludePath, "utf-8") : "";
3488
+ if (!current.includes(MANIFEST_FILENAME)) {
3489
+ fs10.writeFileSync(
3490
+ excludePath,
3491
+ (current.endsWith("\n") || current === "" ? current : `${current}
3492
+ `) + `# skaile-managed local-overlay manifest
3493
+ ${MANIFEST_FILENAME}
3494
+ `,
3495
+ "utf-8"
3545
3496
  );
3546
3497
  }
3547
3498
  }
3548
3499
  }
3549
- return { updated, errors };
3500
+ return { wrote: true, manifestPath };
3550
3501
  }
3551
- function itemKey(item) {
3552
- return item.id ?? item.ref;
3502
+ var PinPolicySchema = z3.enum(PIN_POLICIES);
3503
+ var LibraryInstanceRefSchema = z3.object({
3504
+ id: z3.string().uuid(),
3505
+ alias: z3.string().optional()
3506
+ });
3507
+ var LibrarySectionSchema = z3.object({
3508
+ pin_policy: PinPolicySchema.default("minor-track"),
3509
+ instances: z3.array(LibraryInstanceRefSchema).default([])
3510
+ });
3511
+ var PresetAppliedItemSchema = z3.object({
3512
+ instance_id: z3.string().uuid(),
3513
+ def_ref: z3.string()
3514
+ });
3515
+ var PresetAppliedSchema = z3.object({
3516
+ ref: z3.string(),
3517
+ applied_at: z3.string(),
3518
+ items: z3.array(PresetAppliedItemSchema).default([])
3519
+ });
3520
+ var WorkspaceConfigV2Schema = z3.object({
3521
+ version: z3.literal(2),
3522
+ library: LibrarySectionSchema.optional(),
3523
+ presets_applied: z3.array(PresetAppliedSchema).optional()
3524
+ });
3525
+ var LockAssignmentSchema = z3.object({
3526
+ assignment_id: z3.string().uuid(),
3527
+ instance_id: z3.string().uuid(),
3528
+ def_ref: z3.string(),
3529
+ resolved_version: z3.string(),
3530
+ resolved_sha256: z3.string().optional(),
3531
+ pin_policy: PinPolicySchema
3532
+ });
3533
+ var RequiresEdgeSchema = z3.object({
3534
+ from: z3.string(),
3535
+ to: z3.string(),
3536
+ kind: z3.string().optional()
3537
+ });
3538
+ var PresetExpandedSchema = z3.object({
3539
+ preset_ref: z3.string(),
3540
+ instance_ids: z3.array(z3.string().uuid())
3541
+ });
3542
+ var LockFileSchema = z3.object({
3543
+ version: z3.literal(2),
3544
+ generated_at: z3.string(),
3545
+ assignments: z3.array(LockAssignmentSchema).default([]),
3546
+ requires_graph: z3.array(RequiresEdgeSchema).default([]),
3547
+ presets_expanded: z3.array(PresetExpandedSchema).default([])
3548
+ });
3549
+ function parseWorkspaceV2(raw) {
3550
+ if (raw.version !== 2) return null;
3551
+ return WorkspaceConfigV2Schema.parse(raw);
3553
3552
  }
3554
- function indexItems(items) {
3555
- const map = /* @__PURE__ */ new Map();
3556
- for (const item of items) {
3557
- map.set(itemKey(item), item);
3553
+ function loadWorkspaceV2(dir, filename = "skaile.yaml") {
3554
+ const filePath = path5.join(dir, filename);
3555
+ if (!fs10.existsSync(filePath)) return null;
3556
+ const content = fs10.readFileSync(filePath, "utf-8");
3557
+ const raw = parse(content);
3558
+ if (!raw || typeof raw !== "object") return null;
3559
+ return parseWorkspaceV2(raw);
3560
+ }
3561
+ function saveWorkspaceV2(dir, v2, filename = "skaile.yaml") {
3562
+ const filePath = path5.join(dir, filename);
3563
+ let existing = {};
3564
+ if (fs10.existsSync(filePath)) {
3565
+ const content = fs10.readFileSync(filePath, "utf-8");
3566
+ existing = parse(content) ?? {};
3558
3567
  }
3559
- return map;
3568
+ existing.version = 2;
3569
+ if (v2.library !== void 0) existing.library = v2.library;
3570
+ if (v2.presets_applied !== void 0) existing.presets_applied = v2.presets_applied;
3571
+ if (!fs10.existsSync(dir)) {
3572
+ fs10.mkdirSync(dir, { recursive: true });
3573
+ }
3574
+ fs10.writeFileSync(filePath, stringify(existing, { indent: 2 }), "utf-8");
3560
3575
  }
3561
- function deepEqual(a, b) {
3562
- if (a === b) return true;
3563
- if (a === void 0 && b === void 0) return true;
3564
- if (a === null && b === null) return true;
3565
- if (a === void 0 || b === void 0) return false;
3566
- if (a === null || b === null) return false;
3567
- return JSON.stringify(a) === JSON.stringify(b);
3576
+ var LOCK_FILENAME = "skaile.lock.yaml";
3577
+ function loadLockFile(dir) {
3578
+ const filePath = path5.join(dir, LOCK_FILENAME);
3579
+ if (!fs10.existsSync(filePath)) return null;
3580
+ const content = fs10.readFileSync(filePath, "utf-8");
3581
+ const raw = parse(content);
3582
+ if (raw !== null && !("assignments" in raw) && "subscriptions" in raw) {
3583
+ const legacySubs = raw.subscriptions;
3584
+ raw.assignments = Array.isArray(legacySubs) ? legacySubs.map((s) => {
3585
+ if (s !== null && typeof s === "object" && "subscription_id" in s && !("assignment_id" in s)) {
3586
+ const { subscription_id, ...rest } = s;
3587
+ return { assignment_id: subscription_id, ...rest };
3588
+ }
3589
+ return s;
3590
+ }) : [];
3591
+ delete raw.subscriptions;
3592
+ }
3593
+ return LockFileSchema.parse(raw);
3594
+ }
3595
+ function saveLockFile(dir, lock) {
3596
+ const filePath = path5.join(dir, LOCK_FILENAME);
3597
+ if (!fs10.existsSync(dir)) {
3598
+ fs10.mkdirSync(dir, { recursive: true });
3599
+ }
3600
+ fs10.writeFileSync(
3601
+ filePath,
3602
+ `# Auto-generated by skaile. Do not edit manually.
3603
+ ${stringify(lock, { indent: 2 })}`,
3604
+ "utf-8"
3605
+ );
3606
+ }
3607
+ function createEmptyLockFile() {
3608
+ return {
3609
+ version: 2,
3610
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
3611
+ assignments: [],
3612
+ requires_graph: [],
3613
+ presets_expanded: []
3614
+ };
3615
+ }
3616
+ function detectWorkspaceVersion(dir, filename = "skaile.yaml") {
3617
+ const filePath = path5.join(dir, filename);
3618
+ if (!fs10.existsSync(filePath)) return null;
3619
+ const content = fs10.readFileSync(filePath, "utf-8");
3620
+ const raw = parse(content);
3621
+ if (!raw || typeof raw !== "object") return null;
3622
+ return raw.version === 2 ? 2 : 1;
3568
3623
  }
3569
3624
 
3570
3625
  export { AssignmentNotFoundError, CatalogCache, CatalogConfigSchema, CatalogHttpError, DuplicateAssignmentError, DuplicateLibraryNameError, GitSyncDriver, InstanceHasConsumersError, InstanceNotFoundError, InterpolationError, KNOWN_ASSET_KINDS, KnowledgeManifestSchema, LIBRARY_BACKENDS, LIBRARY_OWNERSHIPS, LIBRARY_STRUCTURES, LOCAL_CATALOG_URL, LibraryConfigSchema, LibraryError, LibraryInstanceRefSchema, LibraryManager, LibrarySectionSchema, LibrarySyncError, LocalCatalogSource, LocalIndex, LocalSyncDriver, LockAssignmentSchema, LockFileSchema, OfflineError, OperationNotPermittedError, PIN_POLICIES, PinPolicySchema, PresetAppliedSchema, PresetExpandedSchema, RemoteCatalogSource, RequiresEdgeSchema, RestCatalogSource, SidecarGit, SkaileConfigSchema, SourceNotFoundError, TarballHashMismatchError, UserLibraryNotFoundError, WorkspaceConfigV2Schema, applyNonStructuralUpgrade, applyPreset, applyPresetFromData, classifyRef, createEmptyLockFile, deriveSlug, detectChanges, detectNestedPresetRefs, detectWorkspaceVersion, evaluateUpgrade, extractLibraryInstanceId, extractReferencedKeys, filterKey, getConfigDefaults, getDefaultValue, getSidecarRoot, getStoreManifestsDir, getStoreRoot, hasInterpolationTokens, interpolate, isLocalCatalogUrl, isSecretPlaceholder, knowledgeKindProvider, listSidecarSlugsOnDisk, loadLockFile, loadWorkspaceV2, migrateSidecarsToStoreIfNeeded, migrateWorkspaceConfig, parsePreset, parsePresetYaml, parseWorkspaceV2, projectConfigPath, queryInstancePickerChoices, resolveAllItems, resolveConfig, resolveIndexPath, resolveItem, resolveLibraryDir, resolvePin, resolveSidecarPaths, saveConfig, saveLockFile, saveWorkspaceV2, sha256Hex, skaileHomeDir, storeSecrets, trpcGetUrl, userConfigPath, validateAllPlaceholders, validateKnowledge, validateNestingDepth, validatePlaceholder, writeManifestIfMissing };
3571
- //# sourceMappingURL=chunk-MIH45XPY.js.map
3572
- //# sourceMappingURL=chunk-MIH45XPY.js.map
3626
+ //# sourceMappingURL=chunk-LA2YSXEE.js.map
3627
+ //# sourceMappingURL=chunk-LA2YSXEE.js.map