opkg 0.6.1 → 0.7.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 (487) hide show
  1. package/README.md +109 -186
  2. package/assets/openpackage_ascii_dark.png +0 -0
  3. package/assets/openpackage_ascii_light.png +0 -0
  4. package/dist/commands/add.js +34 -10
  5. package/dist/commands/add.js.map +1 -1
  6. package/dist/commands/apply.js +16 -0
  7. package/dist/commands/apply.js.map +1 -0
  8. package/dist/commands/delete.js +1 -1
  9. package/dist/commands/delete.js.map +1 -1
  10. package/dist/commands/install.js +177 -8
  11. package/dist/commands/install.js.map +1 -1
  12. package/dist/commands/list.js +2 -2
  13. package/dist/commands/list.js.map +1 -1
  14. package/dist/commands/login.js +1 -1
  15. package/dist/commands/login.js.map +1 -1
  16. package/dist/commands/logout.js +1 -1
  17. package/dist/commands/logout.js.map +1 -1
  18. package/dist/commands/new.js +125 -0
  19. package/dist/commands/new.js.map +1 -0
  20. package/dist/commands/pack.js +7 -13
  21. package/dist/commands/pack.js.map +1 -1
  22. package/dist/commands/pull.js +1 -1
  23. package/dist/commands/pull.js.map +1 -1
  24. package/dist/commands/push.js +1 -1
  25. package/dist/commands/push.js.map +1 -1
  26. package/dist/commands/remove.js +63 -0
  27. package/dist/commands/remove.js.map +1 -0
  28. package/dist/commands/save.js +11 -17
  29. package/dist/commands/save.js.map +1 -1
  30. package/dist/commands/set.js +33 -0
  31. package/dist/commands/set.js.map +1 -0
  32. package/dist/commands/show.js +16 -94
  33. package/dist/commands/show.js.map +1 -1
  34. package/dist/commands/status.js +26 -701
  35. package/dist/commands/status.js.map +1 -1
  36. package/dist/commands/uninstall.js +14 -427
  37. package/dist/commands/uninstall.js.map +1 -1
  38. package/dist/constants/index.js +72 -16
  39. package/dist/constants/index.js.map +1 -1
  40. package/dist/core/add/add-conflict-handler.js +1 -8
  41. package/dist/core/add/add-conflict-handler.js.map +1 -1
  42. package/dist/core/add/add-pipeline.js +12 -10
  43. package/dist/core/add/add-pipeline.js.map +1 -1
  44. package/dist/core/add/add-to-source-pipeline.js +123 -0
  45. package/dist/core/add/add-to-source-pipeline.js.map +1 -0
  46. package/dist/core/add/package-index-updater.js +77 -78
  47. package/dist/core/add/package-index-updater.js.map +1 -1
  48. package/dist/core/add/platform-path-transformer.js +6 -4
  49. package/dist/core/add/platform-path-transformer.js.map +1 -1
  50. package/dist/core/add/source-collector.js +2 -3
  51. package/dist/core/add/source-collector.js.map +1 -1
  52. package/dist/core/apply/apply-pipeline.js +110 -0
  53. package/dist/core/apply/apply-pipeline.js.map +1 -0
  54. package/dist/core/dependency-resolver.js +263 -21
  55. package/dist/core/dependency-resolver.js.map +1 -1
  56. package/dist/core/discovery/file-discovery.js +1 -2
  57. package/dist/core/discovery/file-discovery.js.map +1 -1
  58. package/dist/core/discovery/platform-files-discovery.js +33 -18
  59. package/dist/core/discovery/platform-files-discovery.js.map +1 -1
  60. package/dist/core/flows/flow-executor.js +974 -0
  61. package/dist/core/flows/flow-executor.js.map +1 -0
  62. package/dist/core/flows/flow-inverter.js +442 -0
  63. package/dist/core/flows/flow-inverter.js.map +1 -0
  64. package/dist/core/flows/flow-key-extractor.js +101 -0
  65. package/dist/core/flows/flow-key-extractor.js.map +1 -0
  66. package/dist/core/flows/flow-key-mapper.js +382 -0
  67. package/dist/core/flows/flow-key-mapper.js.map +1 -0
  68. package/dist/core/flows/flow-transforms.js +632 -0
  69. package/dist/core/flows/flow-transforms.js.map +1 -0
  70. package/dist/core/flows/map-pipeline/context.js +73 -0
  71. package/dist/core/flows/map-pipeline/context.js.map +1 -0
  72. package/dist/core/flows/map-pipeline/index.js +156 -0
  73. package/dist/core/flows/map-pipeline/index.js.map +1 -0
  74. package/dist/core/flows/map-pipeline/operations/copy.js +104 -0
  75. package/dist/core/flows/map-pipeline/operations/copy.js.map +1 -0
  76. package/dist/core/flows/map-pipeline/operations/pipe.js +70 -0
  77. package/dist/core/flows/map-pipeline/operations/pipe.js.map +1 -0
  78. package/dist/core/flows/map-pipeline/operations/rename.js +102 -0
  79. package/dist/core/flows/map-pipeline/operations/rename.js.map +1 -0
  80. package/dist/core/flows/map-pipeline/operations/set.js +50 -0
  81. package/dist/core/flows/map-pipeline/operations/set.js.map +1 -0
  82. package/dist/core/flows/map-pipeline/operations/switch.js +79 -0
  83. package/dist/core/flows/map-pipeline/operations/switch.js.map +1 -0
  84. package/dist/core/flows/map-pipeline/operations/transform.js +543 -0
  85. package/dist/core/flows/map-pipeline/operations/transform.js.map +1 -0
  86. package/dist/core/flows/map-pipeline/operations/unset.js +65 -0
  87. package/dist/core/flows/map-pipeline/operations/unset.js.map +1 -0
  88. package/dist/core/flows/map-pipeline/types.js +8 -0
  89. package/dist/core/flows/map-pipeline/types.js.map +1 -0
  90. package/dist/core/flows/map-pipeline/utils.js +278 -0
  91. package/dist/core/flows/map-pipeline/utils.js.map +1 -0
  92. package/dist/core/flows/platform-converter.js +328 -0
  93. package/dist/core/flows/platform-converter.js.map +1 -0
  94. package/dist/core/flows/source-resolver.js +192 -0
  95. package/dist/core/flows/source-resolver.js.map +1 -0
  96. package/dist/core/flows/toml-domain-transforms.js +23 -0
  97. package/dist/core/flows/toml-domain-transforms.js.map +1 -0
  98. package/dist/core/install/bulk-install-pipeline.js +68 -7
  99. package/dist/core/install/bulk-install-pipeline.js.map +1 -1
  100. package/dist/core/install/canonical-plan.js +3 -3
  101. package/dist/core/install/canonical-plan.js.map +1 -1
  102. package/dist/core/install/dry-run.js +3 -3
  103. package/dist/core/install/dry-run.js.map +1 -1
  104. package/dist/core/install/flow-based-installer.js +1158 -0
  105. package/dist/core/install/flow-based-installer.js.map +1 -0
  106. package/dist/core/install/flow-workspace-tracker.js +111 -0
  107. package/dist/core/install/flow-workspace-tracker.js.map +1 -0
  108. package/dist/core/install/format-detector.js +228 -0
  109. package/dist/core/install/format-detector.js.map +1 -0
  110. package/dist/core/install/git-package-loader.js +20 -0
  111. package/dist/core/install/git-package-loader.js.map +1 -0
  112. package/dist/core/install/install-errors.js +1 -1
  113. package/dist/core/install/install-errors.js.map +1 -1
  114. package/dist/core/install/install-flow.js +34 -14
  115. package/dist/core/install/install-flow.js.map +1 -1
  116. package/dist/core/install/install-pipeline.js +52 -17
  117. package/dist/core/install/install-pipeline.js.map +1 -1
  118. package/dist/core/install/install-reporting.js +26 -8
  119. package/dist/core/install/install-reporting.js.map +1 -1
  120. package/dist/core/install/local-source-resolution.js +103 -0
  121. package/dist/core/install/local-source-resolution.js.map +1 -0
  122. package/dist/core/install/marketplace-handler.js +221 -0
  123. package/dist/core/install/marketplace-handler.js.map +1 -0
  124. package/dist/core/install/path-install-pipeline.js +241 -0
  125. package/dist/core/install/path-install-pipeline.js.map +1 -0
  126. package/dist/core/install/path-package-loader.js +116 -0
  127. package/dist/core/install/path-package-loader.js.map +1 -0
  128. package/dist/core/install/plugin-detector.js +72 -0
  129. package/dist/core/install/plugin-detector.js.map +1 -0
  130. package/dist/core/install/plugin-to-universal-converter.js +218 -0
  131. package/dist/core/install/plugin-to-universal-converter.js.map +1 -0
  132. package/dist/core/install/plugin-transformer.js +191 -0
  133. package/dist/core/install/plugin-transformer.js.map +1 -0
  134. package/dist/core/install/version-selection.js +1 -1
  135. package/dist/core/install/version-selection.js.map +1 -1
  136. package/dist/core/openpackage.js +40 -22
  137. package/dist/core/openpackage.js.map +1 -1
  138. package/dist/core/pack/pack-output.js +62 -0
  139. package/dist/core/pack/pack-output.js.map +1 -0
  140. package/dist/core/pack/pack-pipeline.js +186 -0
  141. package/dist/core/pack/pack-pipeline.js.map +1 -0
  142. package/dist/core/package-context.js +45 -70
  143. package/dist/core/package-context.js.map +1 -1
  144. package/dist/core/package-creation.js +203 -0
  145. package/dist/core/package-creation.js.map +1 -0
  146. package/dist/core/package.js +20 -6
  147. package/dist/core/package.js.map +1 -1
  148. package/dist/core/platforms.js +665 -209
  149. package/dist/core/platforms.js.map +1 -1
  150. package/dist/core/push/push-context.js +1 -1
  151. package/dist/core/push/push-context.js.map +1 -1
  152. package/dist/core/push/push-upload.js +2 -2
  153. package/dist/core/push/push-upload.js.map +1 -1
  154. package/dist/core/registry.js +6 -6
  155. package/dist/core/registry.js.map +1 -1
  156. package/dist/core/remote-pull.js +2 -2
  157. package/dist/core/remote-pull.js.map +1 -1
  158. package/dist/core/remove/removal-collector.js +52 -0
  159. package/dist/core/remove/removal-collector.js.map +1 -0
  160. package/dist/core/remove/removal-confirmation.js +39 -0
  161. package/dist/core/remove/removal-confirmation.js.map +1 -0
  162. package/dist/core/remove/remove-from-source-pipeline.js +173 -0
  163. package/dist/core/remove/remove-from-source-pipeline.js.map +1 -0
  164. package/dist/core/save/constants.js +3 -3
  165. package/dist/core/save/constants.js.map +1 -1
  166. package/dist/core/save/flow-based-saver.js +270 -0
  167. package/dist/core/save/flow-based-saver.js.map +1 -0
  168. package/dist/core/save/name-resolution.js +1 -1
  169. package/dist/core/save/name-resolution.js.map +1 -1
  170. package/dist/core/save/package-yml-generator.js +4 -5
  171. package/dist/core/save/package-yml-generator.js.map +1 -1
  172. package/dist/core/save/save-candidate-builder.js +215 -0
  173. package/dist/core/save/save-candidate-builder.js.map +1 -0
  174. package/dist/core/save/save-candidate-loader.js +12 -11
  175. package/dist/core/save/save-candidate-loader.js.map +1 -1
  176. package/dist/core/save/save-conflict-analyzer.js +150 -0
  177. package/dist/core/save/save-conflict-analyzer.js.map +1 -0
  178. package/dist/core/save/save-conflict-resolution.js +28 -14
  179. package/dist/core/save/save-conflict-resolution.js.map +1 -1
  180. package/dist/core/save/save-conflict-resolver.js +31 -275
  181. package/dist/core/save/save-conflict-resolver.js.map +1 -1
  182. package/dist/core/save/save-group-builder.js +52 -0
  183. package/dist/core/save/save-group-builder.js.map +1 -0
  184. package/dist/core/save/save-interactive-resolver.js +190 -0
  185. package/dist/core/save/save-interactive-resolver.js.map +1 -0
  186. package/dist/core/save/save-pipeline.js +58 -34
  187. package/dist/core/save/save-pipeline.js.map +1 -1
  188. package/dist/core/save/save-platform-handler.js +53 -0
  189. package/dist/core/save/save-platform-handler.js.map +1 -0
  190. package/dist/core/save/save-resolution-executor.js +145 -0
  191. package/dist/core/save/save-resolution-executor.js.map +1 -0
  192. package/dist/core/save/save-result-reporter.js +167 -0
  193. package/dist/core/save/save-result-reporter.js.map +1 -0
  194. package/dist/core/save/save-to-source-pipeline.js +154 -0
  195. package/dist/core/save/save-to-source-pipeline.js.map +1 -0
  196. package/dist/core/save/save-versioning.js +4 -4
  197. package/dist/core/save/save-versioning.js.map +1 -1
  198. package/dist/core/save/save-write-coordinator.js +204 -0
  199. package/dist/core/save/save-write-coordinator.js.map +1 -0
  200. package/dist/core/save/save-yml-resolution.js +28 -216
  201. package/dist/core/save/save-yml-resolution.js.map +1 -1
  202. package/dist/core/save/workspace-rename.js +7 -8
  203. package/dist/core/save/workspace-rename.js.map +1 -1
  204. package/dist/core/set/set-output.js +72 -0
  205. package/dist/core/set/set-output.js.map +1 -0
  206. package/dist/core/set/set-pipeline.js +361 -0
  207. package/dist/core/set/set-pipeline.js.map +1 -0
  208. package/dist/core/set/set-types.js +5 -0
  209. package/dist/core/set/set-types.js.map +1 -0
  210. package/dist/core/show/package-resolver.js +257 -0
  211. package/dist/core/show/package-resolver.js.map +1 -0
  212. package/dist/core/show/scope-discovery.js +165 -0
  213. package/dist/core/show/scope-discovery.js.map +1 -0
  214. package/dist/core/show/show-output.js +168 -0
  215. package/dist/core/show/show-output.js.map +1 -0
  216. package/dist/core/show/show-pipeline.js +113 -0
  217. package/dist/core/show/show-pipeline.js.map +1 -0
  218. package/dist/core/show/show-types.js +5 -0
  219. package/dist/core/show/show-types.js.map +1 -0
  220. package/dist/core/source-resolution/dependency-graph.js +104 -0
  221. package/dist/core/source-resolution/dependency-graph.js.map +1 -0
  222. package/dist/core/source-resolution/resolve-mutable-source.js +109 -0
  223. package/dist/core/source-resolution/resolve-mutable-source.js.map +1 -0
  224. package/dist/core/source-resolution/resolve-package-source.js +29 -0
  225. package/dist/core/source-resolution/resolve-package-source.js.map +1 -0
  226. package/dist/core/source-resolution/resolve-registry-version.js +35 -0
  227. package/dist/core/source-resolution/resolve-registry-version.js.map +1 -0
  228. package/dist/core/source-resolution/types.js.map +1 -0
  229. package/dist/core/status/status-file-discovery.js +23 -12
  230. package/dist/core/status/status-file-discovery.js.map +1 -1
  231. package/dist/core/status/status-pipeline.js +134 -0
  232. package/dist/core/status/status-pipeline.js.map +1 -0
  233. package/dist/core/sync/platform-sync-summary.js +27 -0
  234. package/dist/core/sync/platform-sync-summary.js.map +1 -0
  235. package/dist/core/uninstall/flow-aware-uninstaller.js +189 -0
  236. package/dist/core/uninstall/flow-aware-uninstaller.js.map +1 -0
  237. package/dist/core/uninstall/uninstall-file-discovery.js +11 -6
  238. package/dist/core/uninstall/uninstall-file-discovery.js.map +1 -1
  239. package/dist/core/uninstall/uninstall-pipeline.js +141 -0
  240. package/dist/core/uninstall/uninstall-pipeline.js.map +1 -0
  241. package/dist/core/universal-patterns.js +64 -0
  242. package/dist/core/universal-patterns.js.map +1 -0
  243. package/dist/index.js +99 -6
  244. package/dist/index.js.map +1 -1
  245. package/dist/types/flows.js +8 -0
  246. package/dist/types/flows.js.map +1 -0
  247. package/dist/types/index.js +3 -0
  248. package/dist/types/index.js.map +1 -1
  249. package/dist/types/platform-flows.js +8 -0
  250. package/dist/types/platform-flows.js.map +1 -0
  251. package/dist/types/workspace-index.js +6 -0
  252. package/dist/types/workspace-index.js.map +1 -0
  253. package/dist/utils/custom-path-resolution.js +160 -0
  254. package/dist/utils/custom-path-resolution.js.map +1 -0
  255. package/dist/utils/dependency-coverage.js +1 -1
  256. package/dist/utils/dependency-coverage.js.map +1 -1
  257. package/dist/utils/file-processing.js +1 -1
  258. package/dist/utils/flow-index-installer.js +209 -0
  259. package/dist/utils/flow-index-installer.js.map +1 -0
  260. package/dist/utils/formatters.js +47 -1
  261. package/dist/utils/formatters.js.map +1 -1
  262. package/dist/utils/fs.js +17 -0
  263. package/dist/utils/fs.js.map +1 -1
  264. package/dist/utils/git-clone-registry.js +88 -0
  265. package/dist/utils/git-clone-registry.js.map +1 -0
  266. package/dist/utils/git-clone.js +69 -0
  267. package/dist/utils/git-clone.js.map +1 -0
  268. package/dist/utils/git-spec.js +96 -0
  269. package/dist/utils/git-spec.js.map +1 -0
  270. package/dist/utils/http-client.js +7 -0
  271. package/dist/utils/http-client.js.map +1 -1
  272. package/dist/utils/index-based-installer.js +356 -163
  273. package/dist/utils/index-based-installer.js.map +1 -1
  274. package/dist/utils/install-conflict-handler.js +2 -2
  275. package/dist/utils/install-conflict-handler.js.map +1 -1
  276. package/dist/utils/install-file-discovery.js +18 -13
  277. package/dist/utils/install-file-discovery.js.map +1 -1
  278. package/dist/utils/install-helpers.js +43 -20
  279. package/dist/utils/install-helpers.js.map +1 -1
  280. package/dist/utils/jsonc.js +23 -1
  281. package/dist/utils/jsonc.js.map +1 -1
  282. package/dist/utils/manifest-paths.js +1 -1
  283. package/dist/utils/manifest-paths.js.map +1 -1
  284. package/dist/utils/markdown-frontmatter.js +46 -0
  285. package/dist/utils/markdown-frontmatter.js.map +1 -1
  286. package/dist/utils/package-copy.js +5 -103
  287. package/dist/utils/package-copy.js.map +1 -1
  288. package/dist/utils/package-filters.js +9 -105
  289. package/dist/utils/package-filters.js.map +1 -1
  290. package/dist/utils/package-index-yml.js +27 -6
  291. package/dist/utils/package-index-yml.js.map +1 -1
  292. package/dist/utils/package-input.js +98 -0
  293. package/dist/utils/package-input.js.map +1 -0
  294. package/dist/utils/package-management.js +80 -28
  295. package/dist/utils/package-management.js.map +1 -1
  296. package/dist/utils/package-name-resolution.js +327 -0
  297. package/dist/utils/package-name-resolution.js.map +1 -0
  298. package/dist/utils/package-name.js +18 -16
  299. package/dist/utils/package-name.js.map +1 -1
  300. package/dist/utils/package-versioning.js +2 -33
  301. package/dist/utils/package-versioning.js.map +1 -1
  302. package/dist/utils/package-yml.js +19 -28
  303. package/dist/utils/package-yml.js.map +1 -1
  304. package/dist/utils/path-resolution.js +102 -0
  305. package/dist/utils/path-resolution.js.map +1 -0
  306. package/dist/utils/paths.js +6 -6
  307. package/dist/utils/paths.js.map +1 -1
  308. package/dist/utils/platform-file.js +36 -24
  309. package/dist/utils/platform-file.js.map +1 -1
  310. package/dist/utils/platform-mapper.js +222 -68
  311. package/dist/utils/platform-mapper.js.map +1 -1
  312. package/dist/utils/platform-root-files.js +44 -0
  313. package/dist/utils/platform-root-files.js.map +1 -0
  314. package/dist/utils/platform-utils.js +35 -54
  315. package/dist/utils/platform-utils.js.map +1 -1
  316. package/dist/utils/platform-yaml-merge.js +20 -140
  317. package/dist/utils/platform-yaml-merge.js.map +1 -1
  318. package/dist/utils/prompts.js +92 -7
  319. package/dist/utils/prompts.js.map +1 -1
  320. package/dist/utils/registry-entry-filter.js +50 -27
  321. package/dist/utils/registry-entry-filter.js.map +1 -1
  322. package/dist/utils/registry-paths.js +5 -4
  323. package/dist/utils/registry-paths.js.map +1 -1
  324. package/dist/utils/scope-resolution.js +156 -0
  325. package/dist/utils/scope-resolution.js.map +1 -0
  326. package/dist/utils/source-mutability.js +15 -0
  327. package/dist/utils/source-mutability.js.map +1 -0
  328. package/dist/utils/tarball.js +29 -4
  329. package/dist/utils/tarball.js.map +1 -1
  330. package/dist/utils/version-ranges.js +1 -32
  331. package/dist/utils/version-ranges.js.map +1 -1
  332. package/dist/utils/workspace-index-helpers.js +28 -0
  333. package/dist/utils/workspace-index-helpers.js.map +1 -0
  334. package/dist/utils/workspace-index-ownership.js +100 -0
  335. package/dist/utils/workspace-index-ownership.js.map +1 -0
  336. package/dist/utils/workspace-index-yml.js +173 -0
  337. package/dist/utils/workspace-index-yml.js.map +1 -0
  338. package/examples/custom-subdirs-platform.jsonc +157 -0
  339. package/package.json +7 -2
  340. package/platforms.jsonc +531 -84
  341. package/schemas/map-pipeline-v1.json +256 -0
  342. package/schemas/platforms-v1.json +400 -0
  343. package/specs/README.md +88 -0
  344. package/specs/add/README.md +166 -0
  345. package/specs/agents-claude.md +570 -0
  346. package/specs/agents-opencode.md +622 -0
  347. package/specs/apply/README.md +21 -0
  348. package/specs/apply/apply-behavior.md +58 -0
  349. package/specs/apply/apply-command.md +51 -0
  350. package/specs/apply/conflicts.md +41 -0
  351. package/specs/apply/index-effects.md +81 -0
  352. package/specs/architecture.md +107 -0
  353. package/specs/auth/README.md +17 -0
  354. package/specs/auth/auth-http-contract.md +25 -0
  355. package/specs/auth/cli/credentials.md +39 -0
  356. package/specs/auth/cli/login.md +32 -0
  357. package/specs/auth/cli/logout.md +16 -0
  358. package/specs/claude-mcp.md +1065 -0
  359. package/specs/claude-plugins-marketplace.md +363 -0
  360. package/specs/claude-plugins.md +413 -0
  361. package/specs/cli-options.md +52 -0
  362. package/specs/codex-mcp.md +114 -0
  363. package/specs/commands-overview.md +175 -0
  364. package/specs/directory-layout.md +95 -0
  365. package/specs/install/README.md +12 -4
  366. package/specs/install/git-sources.md +230 -0
  367. package/specs/install/install-behavior.md +483 -73
  368. package/specs/install/package-yml-canonical.md +67 -35
  369. package/specs/install/version-resolution.md +69 -115
  370. package/specs/new/README.md +769 -0
  371. package/specs/new/SUMMARY.md +310 -0
  372. package/specs/new/scope-behavior.md +793 -0
  373. package/specs/pack/README.md +77 -0
  374. package/specs/pack/package-name-resolution.md +330 -0
  375. package/specs/package/README.md +18 -17
  376. package/specs/package/nested-packages-and-parent-packages.md +32 -31
  377. package/specs/package/package-index-yml.md +95 -101
  378. package/specs/package/package-root-layout.md +64 -46
  379. package/specs/package/registry-payload-and-copy.md +50 -44
  380. package/specs/package/universal-content.md +33 -56
  381. package/specs/package-sources.md +248 -0
  382. package/specs/platforms/README.md +52 -0
  383. package/specs/platforms/configuration.md +571 -0
  384. package/specs/platforms/detection.md +552 -0
  385. package/specs/platforms/directory-layout.md +599 -0
  386. package/specs/platforms/examples.md +1146 -0
  387. package/specs/platforms/flow-reference.md +1240 -0
  388. package/specs/platforms/flows.md +1488 -0
  389. package/specs/platforms/map-pipeline.md +801 -0
  390. package/specs/platforms/overview.md +349 -0
  391. package/specs/platforms/specification.md +700 -0
  392. package/specs/platforms/troubleshooting.md +697 -0
  393. package/specs/platforms/universal-converter.md +520 -0
  394. package/specs/push/README.md +1 -0
  395. package/specs/push/push-behavior.md +11 -3
  396. package/specs/push/push-remote-upload.md +1 -1
  397. package/specs/push/push-scoping.md +1 -1
  398. package/specs/push/push-version-selection.md +1 -1
  399. package/specs/registry.md +111 -0
  400. package/specs/remove/README.md +257 -0
  401. package/specs/save/README.md +21 -17
  402. package/specs/save/save-conflict-resolution.md +205 -83
  403. package/specs/save/save-file-discovery.md +6 -4
  404. package/specs/save/save-frontmatter-overrides.md +11 -15
  405. package/specs/save/save-modes-inputs.md +9 -39
  406. package/specs/save/save-naming-scoping.md +4 -4
  407. package/specs/save/save-package-detection.md +13 -13
  408. package/specs/save/save-registry-sync.md +16 -106
  409. package/specs/save/save-versioning.md +80 -0
  410. package/specs/scope-management.md +92 -0
  411. package/specs/set/README.md +520 -0
  412. package/specs/set/set-behavior.md +563 -0
  413. package/specs/show/README.md +483 -0
  414. package/specs/show/show-remote.md +494 -0
  415. package/specs/status/README.md +38 -0
  416. package/specs/uninstall/README.md +231 -0
  417. package/dist/commands/duplicate.js +0 -69
  418. package/dist/commands/duplicate.js.map +0 -1
  419. package/dist/commands/init.js +0 -117
  420. package/dist/commands/init.js.map +0 -1
  421. package/dist/commands/prune.js +0 -357
  422. package/dist/commands/prune.js.map +0 -1
  423. package/dist/commands/tui.js +0 -61
  424. package/dist/commands/tui.js.map +0 -1
  425. package/dist/core/install/index.js +0 -3
  426. package/dist/core/install/index.js.map +0 -1
  427. package/dist/core/push/push-single-file.js +0 -56
  428. package/dist/core/push/push-single-file.js.map +0 -1
  429. package/dist/core/save/package-detection.js +0 -147
  430. package/dist/core/save/package-detection.js.map +0 -1
  431. package/dist/core/save/save-single-file.js +0 -124
  432. package/dist/core/save/save-single-file.js.map +0 -1
  433. package/dist/core/token-store.js +0 -73
  434. package/dist/core/token-store.js.map +0 -1
  435. package/dist/tui/app.js +0 -95
  436. package/dist/tui/app.js.map +0 -1
  437. package/dist/tui/components/package-list.js +0 -73
  438. package/dist/tui/components/package-list.js.map +0 -1
  439. package/dist/tui/controller.js +0 -365
  440. package/dist/tui/controller.js.map +0 -1
  441. package/dist/tui/index.js +0 -12
  442. package/dist/tui/index.js.map +0 -1
  443. package/dist/tui/services/file-index.js +0 -64
  444. package/dist/tui/services/file-index.js.map +0 -1
  445. package/dist/tui/services/packages.js +0 -18
  446. package/dist/tui/services/packages.js.map +0 -1
  447. package/dist/tui/services/save.js +0 -21
  448. package/dist/tui/services/save.js.map +0 -1
  449. package/dist/tui/state/app-state.js +0 -15
  450. package/dist/tui/state/app-state.js.map +0 -1
  451. package/dist/tui/state.js +0 -17
  452. package/dist/tui/state.js.map +0 -1
  453. package/dist/tui/types.js.map +0 -1
  454. package/dist/tui/views/add-file-modal.js +0 -129
  455. package/dist/tui/views/add-file-modal.js.map +0 -1
  456. package/dist/tui/views/file-preview.js +0 -44
  457. package/dist/tui/views/file-preview.js.map +0 -1
  458. package/dist/tui/views/list-packages.js +0 -73
  459. package/dist/tui/views/list-packages.js.map +0 -1
  460. package/dist/tui/views/main-menu.js +0 -29
  461. package/dist/tui/views/main-menu.js.map +0 -1
  462. package/dist/tui/views/manage-view.js +0 -81
  463. package/dist/tui/views/manage-view.js.map +0 -1
  464. package/dist/tui/views/package-hub.js +0 -120
  465. package/dist/tui/views/package-hub.js.map +0 -1
  466. package/dist/tui/views/placeholder.js +0 -24
  467. package/dist/tui/views/placeholder.js.map +0 -1
  468. package/dist/utils/bun-bootstrap.js +0 -72
  469. package/dist/utils/bun-bootstrap.js.map +0 -1
  470. package/dist/utils/entity-id.js +0 -19
  471. package/dist/utils/entity-id.js.map +0 -1
  472. package/dist/utils/package-local-files.js +0 -5
  473. package/dist/utils/package-local-files.js.map +0 -1
  474. package/dist/utils/path-matching.js +0 -74
  475. package/dist/utils/path-matching.js.map +0 -1
  476. package/dist/utils/root-file-operations.js +0 -39
  477. package/dist/utils/root-file-operations.js.map +0 -1
  478. package/dist/utils/root-file-transformer.js +0 -27
  479. package/dist/utils/root-file-transformer.js.map +0 -1
  480. package/dist/utils/yaml-frontmatter.js +0 -25
  481. package/dist/utils/yaml-frontmatter.js.map +0 -1
  482. package/specs/auth/auth-device-flow.md +0 -70
  483. package/specs/login/login-device-flow.md +0 -70
  484. package/specs/platforms.md +0 -193
  485. package/specs/save-pack-versioning.md +0 -224
  486. package/specs/save-pack.md +0 -68
  487. /package/dist/{tui → core/source-resolution}/types.js +0 -0
@@ -0,0 +1,1158 @@
1
+ /**
2
+ * Flow-Based Installer Module
3
+ *
4
+ * Handles installation of package files using the declarative flow system.
5
+ * Integrates with the existing install pipeline to execute flow transformations
6
+ * for each package file, with multi-package composition and priority-based merging.
7
+ */
8
+ import { join, dirname, basename, relative, extname } from 'path';
9
+ import { promises as fs } from 'fs';
10
+ import { getPlatformDefinition, getGlobalExportFlows, platformUsesFlows, getAllPlatforms, isPlatformId } from '../platforms.js';
11
+ import { createFlowExecutor } from '../flows/flow-executor.js';
12
+ import { exists, ensureDir } from '../../utils/fs.js';
13
+ import { logger } from '../../utils/logger.js';
14
+ import { toTildePath } from '../../utils/path-resolution.js';
15
+ import { minimatch } from 'minimatch';
16
+ import { parseUniversalPath } from '../../utils/platform-file.js';
17
+ import { detectPackageFormat, shouldInstallDirectly, shouldUsePathMappingOnly, needsConversion } from '../install/format-detector.js';
18
+ import { createPlatformConverter } from '../flows/platform-converter.js';
19
+ // ============================================================================
20
+ // Helpers
21
+ // ============================================================================
22
+ /**
23
+ * Get the first pattern from a flow's from field
24
+ * For array patterns, returns the first pattern; for string, returns as-is
25
+ */
26
+ function getFirstFromPattern(from) {
27
+ return Array.isArray(from) ? from[0] : from;
28
+ }
29
+ // ============================================================================
30
+ // Platform Suffix Detection Helpers
31
+ // ============================================================================
32
+ /**
33
+ * Extract platform suffix from filename (e.g., "mcp.claude.jsonc" -> "claude")
34
+ * Works for both root-level files and files in subdirectories
35
+ */
36
+ function extractPlatformSuffixFromFilename(filename) {
37
+ const knownPlatforms = getAllPlatforms({ includeDisabled: true });
38
+ const baseName = basename(filename);
39
+ const parts = baseName.split('.');
40
+ // Need at least 3 parts: name.platform.ext
41
+ if (parts.length >= 3) {
42
+ const possiblePlatform = parts[parts.length - 2];
43
+ if (isPlatformId(possiblePlatform)) {
44
+ return possiblePlatform;
45
+ }
46
+ }
47
+ return null;
48
+ }
49
+ /**
50
+ * Strip platform suffix from filename (e.g., "mcp.claude.jsonc" -> "mcp.jsonc")
51
+ */
52
+ function stripPlatformSuffixFromFilename(filename) {
53
+ const platformSuffix = extractPlatformSuffixFromFilename(filename);
54
+ if (!platformSuffix) {
55
+ return filename;
56
+ }
57
+ const dir = dirname(filename);
58
+ const baseName = basename(filename);
59
+ const parts = baseName.split('.');
60
+ // Remove platform suffix (second-to-last part)
61
+ const strippedParts = [...parts.slice(0, -2), parts[parts.length - 1]];
62
+ const strippedBaseName = strippedParts.join('.');
63
+ return dir === '.' ? strippedBaseName : join(dir, strippedBaseName);
64
+ }
65
+ // ============================================================================
66
+ // Flow Discovery
67
+ // ============================================================================
68
+ /**
69
+ * Get applicable flows for a platform, including global flows
70
+ */
71
+ function getApplicableFlows(platform, cwd) {
72
+ const flows = [];
73
+ // Add global export flows first (applied before platform-specific)
74
+ const globalExportFlows = getGlobalExportFlows(cwd);
75
+ if (globalExportFlows && globalExportFlows.length > 0) {
76
+ flows.push(...globalExportFlows);
77
+ }
78
+ // Add platform-specific export flows
79
+ const definition = getPlatformDefinition(platform, cwd);
80
+ if (definition.export && definition.export.length > 0) {
81
+ flows.push(...definition.export);
82
+ }
83
+ return flows;
84
+ }
85
+ /**
86
+ * Discover source files that match flow patterns
87
+ * Resolves {name} placeholders and glob patterns
88
+ */
89
+ async function discoverFlowSources(flows, packageRoot, context) {
90
+ const flowSources = new Map();
91
+ for (const flow of flows) {
92
+ const firstPattern = getFirstFromPattern(flow.from);
93
+ const sourcePattern = resolvePattern(firstPattern, context);
94
+ const sourcePaths = await matchPattern(sourcePattern, packageRoot);
95
+ flowSources.set(flow, sourcePaths);
96
+ }
97
+ return flowSources;
98
+ }
99
+ /**
100
+ * Resolve pattern placeholders like {name}
101
+ * Note: {name} is reserved for pattern matching and is NOT replaced
102
+ * unless explicitly provided in the context variables
103
+ */
104
+ function resolvePattern(pattern, context, capturedName) {
105
+ return pattern.replace(/{(\w+)}/g, (match, key) => {
106
+ // If capturedName is provided and this is {name}, use the captured value
107
+ if (key === 'name' && capturedName !== undefined) {
108
+ return capturedName;
109
+ }
110
+ // Otherwise, reserve {name} for pattern matching - don't substitute it
111
+ if (key === 'name') {
112
+ return match;
113
+ }
114
+ if (key in context.variables) {
115
+ return String(context.variables[key]);
116
+ }
117
+ return match;
118
+ });
119
+ }
120
+ /**
121
+ * Extract the captured {name} value from a source path that matched a pattern
122
+ * For example: sourcePath="rules/typescript.md", pattern="rules/{name}.md" → "typescript"
123
+ */
124
+ function extractCapturedName(sourcePath, pattern) {
125
+ // Convert pattern to regex with capture group for {name}
126
+ const regexPattern = pattern
127
+ .replace(/\{name\}/g, '([^/]+)')
128
+ .replace(/\*/g, '.*')
129
+ .replace(/\./g, '\\.');
130
+ const regex = new RegExp('^' + regexPattern + '$');
131
+ const match = sourcePath.match(regex);
132
+ if (match && match[1]) {
133
+ return match[1];
134
+ }
135
+ return undefined;
136
+ }
137
+ /**
138
+ * Match files against a pattern
139
+ * Supports simple patterns with {name} placeholders and * wildcards
140
+ * Also discovers platform-specific variant files (e.g., mcp.claude.jsonc for mcp.jsonc)
141
+ */
142
+ async function matchPattern(pattern, baseDir) {
143
+ const matches = [];
144
+ // Fast path: no wildcards/placeholders, check exact file and platform-specific variants
145
+ if (!pattern.includes('*') && !pattern.includes('{')) {
146
+ const exactPath = join(baseDir, pattern);
147
+ // Check for exact match
148
+ if (await exists(exactPath)) {
149
+ matches.push(relative(baseDir, exactPath));
150
+ }
151
+ // Also check for platform-specific variants (e.g., mcp.claude.jsonc, mcp.cursor.jsonc)
152
+ const dirPath = dirname(exactPath);
153
+ const fileName = basename(exactPath);
154
+ const nameParts = fileName.split('.');
155
+ if (nameParts.length >= 2 && await exists(dirPath)) {
156
+ // Get all known platforms
157
+ const knownPlatforms = getAllPlatforms({ includeDisabled: true });
158
+ // For each platform, check if a platform-specific variant exists
159
+ // Pattern: name.platform.ext (e.g., mcp.claude.jsonc)
160
+ const ext = nameParts[nameParts.length - 1];
161
+ const baseName = nameParts.slice(0, -1).join('.');
162
+ for (const platform of knownPlatforms) {
163
+ const platformFileName = `${baseName}.${platform}.${ext}`;
164
+ const platformPath = join(dirPath, platformFileName);
165
+ if (await exists(platformPath)) {
166
+ matches.push(relative(baseDir, platformPath));
167
+ }
168
+ }
169
+ }
170
+ return matches;
171
+ }
172
+ // Globs: reuse a minimatch-based recursive walk similar to flow-executor.ts
173
+ const parts = pattern.split('/');
174
+ const globPart = parts.findIndex(p => p.includes('*'));
175
+ // No glob segment (e.g. {name}.md): scan the parent dir and filter
176
+ if (globPart === -1) {
177
+ const dirRel = dirname(pattern);
178
+ const filePattern = basename(pattern);
179
+ const searchDir = join(baseDir, dirRel);
180
+ if (!(await exists(searchDir)))
181
+ return [];
182
+ const entries = await fs.readdir(searchDir, { withFileTypes: true });
183
+ const regex = new RegExp('^' +
184
+ filePattern
185
+ .replace(/\{name\}/g, '([^/]+)')
186
+ .replace(/\./g, '\\.')
187
+ .replace(/\*/g, '.*') +
188
+ '$');
189
+ for (const entry of entries) {
190
+ if (!entry.isFile())
191
+ continue;
192
+ if (!regex.test(entry.name))
193
+ continue;
194
+ matches.push(relative(baseDir, join(searchDir, entry.name)));
195
+ }
196
+ return matches;
197
+ }
198
+ const dirPath = join(baseDir, ...parts.slice(0, globPart));
199
+ const filePattern = parts.slice(globPart).join('/');
200
+ if (!(await exists(dirPath))) {
201
+ return [];
202
+ }
203
+ await findMatchingFiles(dirPath, filePattern, baseDir, matches);
204
+ return matches;
205
+ }
206
+ async function findMatchingFiles(dir, pattern, baseDir, matches) {
207
+ try {
208
+ const entries = await fs.readdir(dir, { withFileTypes: true });
209
+ for (const entry of entries) {
210
+ const fullPath = join(dir, entry.name);
211
+ const rel = relative(baseDir, fullPath);
212
+ if (entry.isDirectory()) {
213
+ await findMatchingFiles(fullPath, pattern, baseDir, matches);
214
+ }
215
+ else if (entry.isFile()) {
216
+ if (minimatch(rel, pattern, { dot: false })) {
217
+ matches.push(rel);
218
+ }
219
+ }
220
+ }
221
+ }
222
+ catch {
223
+ // ignore
224
+ }
225
+ }
226
+ function resolveTargetFromGlob(sourceAbsPath, fromPattern, toPattern, context) {
227
+ const sourceRelFromPackage = relative(context.packageRoot, sourceAbsPath);
228
+ // If 'to' pattern has glob, map the structure
229
+ if (toPattern.includes('*')) {
230
+ // Handle ** recursive patterns
231
+ if (fromPattern.includes('**') && toPattern.includes('**')) {
232
+ const fromParts = fromPattern.split('**');
233
+ const toParts = toPattern.split('**');
234
+ const fromBase = fromParts[0].replace(/\/$/, '');
235
+ const toBase = toParts[0].replace(/\/$/, '');
236
+ const fromSuffix = fromParts[1] || '';
237
+ const toSuffix = toParts[1] || '';
238
+ let relativeSubpath = sourceRelFromPackage;
239
+ if (fromBase) {
240
+ relativeSubpath = sourceRelFromPackage.startsWith(fromBase + '/')
241
+ ? sourceRelFromPackage.slice(fromBase.length + 1)
242
+ : sourceRelFromPackage;
243
+ }
244
+ // Handle extension mapping if suffixes specify extensions: /**/*.md -> /**/*.mdc
245
+ if (fromSuffix && toSuffix) {
246
+ const fromExt = fromSuffix.replace(/^\/?\*+/, '');
247
+ const toExt = toSuffix.replace(/^\/?\*+/, '');
248
+ if (fromExt && toExt && fromExt !== toExt) {
249
+ relativeSubpath = relativeSubpath.replace(new RegExp(fromExt.replace('.', '\\.') + '$'), toExt);
250
+ }
251
+ }
252
+ const targetPath = toBase ? join(toBase, relativeSubpath) : relativeSubpath;
253
+ return join(context.workspaceRoot, targetPath);
254
+ }
255
+ // Single-level * patterns
256
+ const sourceExt = extname(sourceAbsPath);
257
+ const sourceBase = basename(sourceAbsPath, sourceExt);
258
+ const toParts = toPattern.split('*');
259
+ const toPrefix = toParts[0];
260
+ const toSuffix = toParts[1] || '';
261
+ const targetExt = toSuffix.startsWith('.') ? toSuffix : (sourceExt + toSuffix);
262
+ const targetFileName = sourceBase + targetExt;
263
+ return join(context.workspaceRoot, toPrefix + targetFileName);
264
+ }
265
+ // No glob in target - use as-is
266
+ return join(context.workspaceRoot, toPattern);
267
+ }
268
+ // ============================================================================
269
+ // Flow Execution
270
+ // ============================================================================
271
+ /**
272
+ * Execute flows for a single package installation with format detection and conversion
273
+ */
274
+ export async function installPackageWithFlows(installContext, options) {
275
+ const { packageName, packageRoot, workspaceRoot, platform, packageVersion, priority, dryRun } = installContext;
276
+ const result = {
277
+ success: true,
278
+ filesProcessed: 0,
279
+ filesWritten: 0,
280
+ conflicts: [],
281
+ errors: [],
282
+ targetPaths: [],
283
+ fileMapping: {}
284
+ };
285
+ try {
286
+ // Check if platform uses flows
287
+ if (!platformUsesFlows(platform, workspaceRoot)) {
288
+ // Fall back to subdirs-based installation
289
+ logger.debug(`Platform ${platform} does not use flows, skipping flow-based installation`);
290
+ return result;
291
+ }
292
+ // Phase 1: Get or detect package format
293
+ const packageFormat = installContext.packageFormat || await detectPackageFormatFromDirectory(packageRoot);
294
+ logger.info('Package format determination', {
295
+ providedFormat: installContext.packageFormat ? 'yes' : 'no',
296
+ providedType: installContext.packageFormat?.type,
297
+ providedPlatform: installContext.packageFormat?.platform,
298
+ finalType: packageFormat.type,
299
+ finalPlatform: packageFormat.platform
300
+ });
301
+ logger.debug('Package format', {
302
+ package: packageName,
303
+ type: packageFormat.type,
304
+ platform: packageFormat.platform,
305
+ confidence: packageFormat.confidence,
306
+ isNativeFormat: packageFormat.isNativeFormat,
307
+ nativePlatform: packageFormat.nativePlatform,
308
+ targetPlatform: platform,
309
+ source: installContext.packageFormat ? 'provided' : 'detected'
310
+ });
311
+ // Phase 2: Check if path-mapping-only installation (native format)
312
+ if (shouldUsePathMappingOnly(packageFormat, platform)) {
313
+ logger.info(`Installing ${packageName} for ${platform} with path mapping only (native format, no content transforms)`);
314
+ return await installWithPathMappingOnly(installContext, packageFormat, options);
315
+ }
316
+ // Phase 3: Check if direct installation (no conversion, no path mapping)
317
+ if (shouldInstallDirectly(packageFormat, platform)) {
318
+ logger.info(`Installing ${packageName} AS-IS for ${platform} platform (matching format)`);
319
+ return await installDirectly(installContext, packageFormat);
320
+ }
321
+ // Phase 5: Check if conversion needed
322
+ if (needsConversion(packageFormat, platform)) {
323
+ logger.info(`Converting ${packageName} from ${packageFormat.platform} to ${platform} format`);
324
+ return await installWithConversion(installContext, packageFormat, options);
325
+ }
326
+ // Phase 7: Standard flow-based installation (universal format)
327
+ // This is the original behavior for universal packages
328
+ logger.debug(`Standard flow-based installation for ${packageName}`);
329
+ // Get applicable flows
330
+ const flows = getApplicableFlows(platform, workspaceRoot);
331
+ if (flows.length === 0) {
332
+ logger.debug(`No flows defined for platform ${platform}`);
333
+ return result;
334
+ }
335
+ // Create flow executor
336
+ const executor = createFlowExecutor();
337
+ // Get platform definition for accessing rootFile and other metadata
338
+ const platformDef = getPlatformDefinition(platform, workspaceRoot);
339
+ // Build flow context
340
+ const flowContext = {
341
+ workspaceRoot,
342
+ packageRoot,
343
+ platform,
344
+ packageName,
345
+ direction: 'install',
346
+ variables: {
347
+ name: packageName,
348
+ version: packageVersion,
349
+ priority,
350
+ rootFile: platformDef.rootFile,
351
+ rootDir: platformDef.rootDir
352
+ },
353
+ dryRun
354
+ };
355
+ // Discover source files for each flow
356
+ const flowSources = await discoverFlowSources(flows, packageRoot, flowContext);
357
+ // Build a map of base paths to platforms that have override files
358
+ // This allows universal files to exclude platforms that have platform-specific overrides
359
+ const overridesByBasePath = new Map();
360
+ for (const [flow, sources] of flowSources) {
361
+ for (const sourceRel of sources) {
362
+ const parsed = parseUniversalPath(sourceRel, { allowPlatformSuffix: true });
363
+ const platformSuffix = parsed?.platformSuffix || extractPlatformSuffixFromFilename(sourceRel);
364
+ if (platformSuffix) {
365
+ // For universal subdir files, use the parsed baseKey
366
+ // For root-level files, use the stripped filename as the baseKey
367
+ const baseKey = parsed
368
+ ? `${parsed.universalSubdir}/${parsed.relPath}`
369
+ : stripPlatformSuffixFromFilename(sourceRel);
370
+ if (!overridesByBasePath.has(baseKey)) {
371
+ overridesByBasePath.set(baseKey, new Set());
372
+ }
373
+ overridesByBasePath.get(baseKey).add(platformSuffix);
374
+ }
375
+ }
376
+ }
377
+ // Execute flows per *concrete source file* (avoid re-expanding globs inside executor)
378
+ for (const [flow, sources] of flowSources) {
379
+ for (const sourceRel of sources) {
380
+ const sourceAbs = join(packageRoot, sourceRel);
381
+ // Check for platform-specific file suffix (e.g., commands/foo.claude.md or mcp.claude.jsonc)
382
+ // Parse with allowPlatformSuffix to detect and strip platform suffix
383
+ const parsed = parseUniversalPath(sourceRel, { allowPlatformSuffix: true });
384
+ // For files without universal subdir prefix, check platform suffix directly from filename
385
+ const platformSuffix = parsed?.platformSuffix || extractPlatformSuffixFromFilename(sourceRel);
386
+ const isUniversalSubdirFile = parsed !== null;
387
+ // If file has platform suffix, only process for that specific platform
388
+ if (platformSuffix) {
389
+ const filePlatform = platformSuffix;
390
+ if (filePlatform !== platform) {
391
+ // This file is for a different platform, skip it
392
+ logger.debug(`Skipping ${sourceRel} for platform ${platform} (file is for ${filePlatform})`);
393
+ continue;
394
+ }
395
+ // File is for current platform - continue processing
396
+ }
397
+ else if (isUniversalSubdirFile && parsed) {
398
+ // Universal file with subdir: check if there's a platform-specific override for current platform
399
+ const baseKey = `${parsed.universalSubdir}/${parsed.relPath}`;
400
+ const overridePlatforms = overridesByBasePath.get(baseKey);
401
+ if (overridePlatforms && overridePlatforms.has(platform)) {
402
+ // This universal file is overridden by a platform-specific file for this platform
403
+ logger.debug(`Skipping universal file ${sourceRel} for platform ${platform} (overridden by platform-specific file)`);
404
+ continue;
405
+ }
406
+ }
407
+ else {
408
+ // Root-level file without platform suffix: check if there's a platform-specific override
409
+ const strippedFileName = stripPlatformSuffixFromFilename(sourceRel);
410
+ // Check if any file in sources is a platform-specific override for this file
411
+ const hasOverrideForPlatform = sources.some(s => {
412
+ const sSuffix = extractPlatformSuffixFromFilename(s);
413
+ const sStripped = stripPlatformSuffixFromFilename(s);
414
+ return sSuffix === platform && sStripped === strippedFileName;
415
+ });
416
+ if (hasOverrideForPlatform) {
417
+ // This universal file is overridden by a platform-specific file for this platform
418
+ logger.debug(`Skipping universal file ${sourceRel} for platform ${platform} (overridden by platform-specific file)`);
419
+ continue;
420
+ }
421
+ }
422
+ try {
423
+ // Use suffix-stripped path if available, otherwise use original
424
+ // This is the path used for flow pattern matching and target path resolution
425
+ const sourceRelForMapping = parsed ? `${parsed.universalSubdir}/${parsed.relPath}` : sourceRel;
426
+ const sourceAbsForMapping = parsed ? join(packageRoot, sourceRelForMapping) : sourceAbs;
427
+ const firstPattern = getFirstFromPattern(flow.from);
428
+ const capturedName = extractCapturedName(sourceRelForMapping, firstPattern);
429
+ const sourceContext = {
430
+ ...flowContext,
431
+ variables: {
432
+ ...flowContext.variables,
433
+ sourcePath: sourceRelForMapping,
434
+ sourceDir: dirname(sourceRelForMapping),
435
+ sourceFile: basename(sourceRelForMapping),
436
+ ...(capturedName ? { capturedName } : {})
437
+ }
438
+ };
439
+ // Resolve a concrete target path so flow-executor doesn't need glob expansion.
440
+ // Use the suffix-stripped source path for target resolution
441
+ const rawToPattern = typeof flow.to === 'string' ? flow.to : Object.keys(flow.to)[0] ?? '';
442
+ const resolvedToPattern = resolvePattern(rawToPattern, sourceContext, capturedName);
443
+ const targetAbs = resolveTargetFromGlob(sourceAbsForMapping, firstPattern, resolvedToPattern, sourceContext);
444
+ const targetRel = relative(workspaceRoot, targetAbs);
445
+ // Create a concrete flow using the original source path for file reading
446
+ // but with target path computed from the suffix-stripped source
447
+ // The flow executor will resolve flow.from relative to packageRoot
448
+ const concreteFlow = {
449
+ ...flow,
450
+ from: sourceRel, // Original source path (may have platform suffix) for file reading
451
+ to: targetRel // Target path computed from stripped source
452
+ };
453
+ const flowResult = await executor.executeFlow(concreteFlow, sourceContext);
454
+ const wasSkipped = flowResult.warnings?.includes('Flow skipped due to condition');
455
+ if (!wasSkipped) {
456
+ result.filesProcessed++;
457
+ }
458
+ if (flowResult.success && !wasSkipped) {
459
+ const target = typeof flowResult.target === 'string' ? flowResult.target : flowResult.target;
460
+ if (typeof target === 'string') {
461
+ result.targetPaths.push(target);
462
+ const targetRelFromWorkspace = relative(workspaceRoot, target);
463
+ if (!result.fileMapping[sourceRel])
464
+ result.fileMapping[sourceRel] = [];
465
+ const normalizedTargetRel = targetRelFromWorkspace.replace(/\\/g, '/');
466
+ const isKeyTrackedMerge = (flowResult.merge === 'deep' || flowResult.merge === 'shallow') &&
467
+ Array.isArray(flowResult.keys);
468
+ if (isKeyTrackedMerge) {
469
+ result.fileMapping[sourceRel].push({
470
+ target: normalizedTargetRel,
471
+ merge: flowResult.merge,
472
+ keys: flowResult.keys
473
+ });
474
+ }
475
+ else {
476
+ result.fileMapping[sourceRel].push(normalizedTargetRel);
477
+ }
478
+ }
479
+ if (!dryRun) {
480
+ result.filesWritten++;
481
+ }
482
+ if (flowResult.conflicts && flowResult.conflicts.length > 0) {
483
+ for (const conflict of flowResult.conflicts) {
484
+ const packages = [];
485
+ packages.push({ packageName: conflict.winner, priority: 0, chosen: true });
486
+ for (const loser of conflict.losers) {
487
+ packages.push({ packageName: loser, priority: 0, chosen: false });
488
+ }
489
+ result.conflicts.push({
490
+ targetPath: conflict.path,
491
+ packages,
492
+ message: `Conflict in ${conflict.path}: ${conflict.winner} overwrites ${conflict.losers.join(', ')}`
493
+ });
494
+ }
495
+ }
496
+ }
497
+ else if (!flowResult.success) {
498
+ result.success = false;
499
+ result.errors.push({
500
+ flow,
501
+ sourcePath: sourceRel,
502
+ error: flowResult.error || new Error('Unknown error'),
503
+ message: `Failed to execute flow for ${sourceRel}: ${flowResult.error?.message || 'Unknown error'}`
504
+ });
505
+ }
506
+ }
507
+ catch (error) {
508
+ result.success = false;
509
+ result.errors.push({
510
+ flow,
511
+ sourcePath: sourceRel,
512
+ error: error,
513
+ message: `Error processing ${sourceRel}: ${error.message}`
514
+ });
515
+ }
516
+ }
517
+ }
518
+ // Log results
519
+ if (result.filesProcessed > 0) {
520
+ logger.info(`Processed ${result.filesProcessed} files for ${packageName} on platform ${platform}` +
521
+ (dryRun ? ' (dry run)' : `, wrote ${result.filesWritten} files`));
522
+ }
523
+ // Log conflicts
524
+ if (result.conflicts.length > 0) {
525
+ logger.warn(`Detected ${result.conflicts.length} conflicts during installation`);
526
+ for (const conflict of result.conflicts) {
527
+ const winner = conflict.packages.find(p => p.chosen);
528
+ logger.warn(` ${toTildePath(conflict.targetPath)}: ${winner?.packageName} (priority ${winner?.priority}) overwrites ` +
529
+ `${conflict.packages.find(p => !p.chosen)?.packageName}`);
530
+ }
531
+ }
532
+ // Log errors
533
+ if (result.errors.length > 0) {
534
+ logger.error(`Encountered ${result.errors.length} errors during installation`);
535
+ for (const error of result.errors) {
536
+ logger.error(` ${error.sourcePath}: ${error.message}`);
537
+ }
538
+ }
539
+ }
540
+ catch (error) {
541
+ result.success = false;
542
+ logger.error(`Failed to install package ${packageName} with flows: ${error.message}`);
543
+ }
544
+ return result;
545
+ }
546
+ /**
547
+ * Execute flows for multiple packages with priority-based merging
548
+ */
549
+ export async function installPackagesWithFlows(packages, workspaceRoot, platform, options) {
550
+ const aggregatedResult = {
551
+ success: true,
552
+ filesProcessed: 0,
553
+ filesWritten: 0,
554
+ conflicts: [],
555
+ errors: [],
556
+ targetPaths: [],
557
+ fileMapping: {}
558
+ };
559
+ const dryRun = options?.dryRun ?? false;
560
+ // Sort packages by priority (LOWER priority first, so higher priority writes last and wins)
561
+ const sortedPackages = [...packages].sort((a, b) => a.priority - b.priority);
562
+ // Track files written by each package for conflict detection
563
+ const fileTargets = new Map();
564
+ // Install each package
565
+ for (const pkg of sortedPackages) {
566
+ const installContext = {
567
+ packageName: pkg.packageName,
568
+ packageRoot: pkg.packageRoot,
569
+ workspaceRoot,
570
+ platform,
571
+ packageVersion: pkg.packageVersion,
572
+ priority: pkg.priority,
573
+ dryRun
574
+ };
575
+ // Get flows and discover target files to track conflicts
576
+ const flows = getApplicableFlows(platform, workspaceRoot);
577
+ const flowContext = {
578
+ workspaceRoot,
579
+ packageRoot: pkg.packageRoot,
580
+ platform,
581
+ packageName: pkg.packageName,
582
+ direction: 'install',
583
+ variables: {
584
+ name: pkg.packageName,
585
+ version: pkg.packageVersion,
586
+ priority: pkg.priority
587
+ },
588
+ dryRun
589
+ };
590
+ // Discover target paths for this package
591
+ const flowSources = await discoverFlowSources(flows, pkg.packageRoot, flowContext);
592
+ for (const [flow, sources] of flowSources) {
593
+ if (sources.length > 0) {
594
+ // Determine target path from flow
595
+ const targetPath = typeof flow.to === 'string'
596
+ ? resolvePattern(flow.to, flowContext)
597
+ : Object.keys(flow.to)[0]; // For multi-target, use first target
598
+ // Track this package writing to this target
599
+ if (!fileTargets.has(targetPath)) {
600
+ fileTargets.set(targetPath, []);
601
+ }
602
+ fileTargets.get(targetPath).push({
603
+ packageName: pkg.packageName,
604
+ priority: pkg.priority
605
+ });
606
+ }
607
+ }
608
+ const result = await installPackageWithFlows(installContext, options);
609
+ // Aggregate results
610
+ aggregatedResult.filesProcessed += result.filesProcessed;
611
+ aggregatedResult.filesWritten += result.filesWritten;
612
+ aggregatedResult.errors.push(...result.errors);
613
+ aggregatedResult.targetPaths.push(...(result.targetPaths ?? []));
614
+ for (const [source, targets] of Object.entries(result.fileMapping ?? {})) {
615
+ const existing = aggregatedResult.fileMapping[source] ?? [];
616
+ aggregatedResult.fileMapping[source] = Array.from(new Set([...existing, ...targets])).sort();
617
+ }
618
+ if (!result.success) {
619
+ aggregatedResult.success = false;
620
+ }
621
+ }
622
+ // Detect conflicts: files written by multiple packages
623
+ for (const [targetPath, writers] of fileTargets) {
624
+ if (writers.length > 1) {
625
+ // Sort by priority to determine winner
626
+ const sortedWriters = [...writers].sort((a, b) => b.priority - a.priority);
627
+ const winner = sortedWriters[0];
628
+ aggregatedResult.conflicts.push({
629
+ targetPath,
630
+ packages: sortedWriters.map((w, i) => ({
631
+ packageName: w.packageName,
632
+ priority: w.priority,
633
+ chosen: i === 0 // First in sorted list (highest priority) is chosen
634
+ })),
635
+ message: `Conflict in ${targetPath}: ${winner.packageName} (priority ${winner.priority}) overwrites ${sortedWriters.slice(1).map(w => w.packageName).join(', ')}`
636
+ });
637
+ }
638
+ }
639
+ return aggregatedResult;
640
+ }
641
+ // ============================================================================
642
+ // Format Detection and Conversion Helpers
643
+ // ============================================================================
644
+ /**
645
+ * Detect package format from directory by reading files
646
+ */
647
+ async function detectPackageFormatFromDirectory(packageRoot) {
648
+ const files = [];
649
+ // Read all files in package directory
650
+ try {
651
+ for await (const fullPath of walkFiles(packageRoot)) {
652
+ const relativePath = relative(packageRoot, fullPath);
653
+ // Skip git metadata and junk files
654
+ if (relativePath.startsWith('.git/') || relativePath === '.git') {
655
+ continue;
656
+ }
657
+ const pathParts = relativePath.split('/');
658
+ const isJunk = await import('junk').then(m => m.isJunk);
659
+ if (pathParts.some(part => isJunk(part))) {
660
+ continue;
661
+ }
662
+ files.push({
663
+ path: relativePath,
664
+ content: '' // We only need paths for format detection
665
+ });
666
+ }
667
+ }
668
+ catch (error) {
669
+ logger.error('Failed to read package directory for format detection', { packageRoot, error });
670
+ }
671
+ return detectPackageFormat(files);
672
+ }
673
+ /**
674
+ * Helper to walk files in directory
675
+ */
676
+ async function* walkFiles(dir) {
677
+ const entries = await fs.readdir(dir, { withFileTypes: true });
678
+ for (const entry of entries) {
679
+ const fullPath = join(dir, entry.name);
680
+ if (entry.isDirectory()) {
681
+ yield* walkFiles(fullPath);
682
+ }
683
+ else if (entry.isFile()) {
684
+ yield fullPath;
685
+ }
686
+ }
687
+ }
688
+ /**
689
+ * Install package directly without flow transformations (AS-IS installation)
690
+ * Used when source platform = target platform
691
+ */
692
+ async function installDirectly(installContext, packageFormat) {
693
+ const { packageName, packageRoot, workspaceRoot, platform, dryRun } = installContext;
694
+ const result = {
695
+ success: true,
696
+ filesProcessed: 0,
697
+ filesWritten: 0,
698
+ conflicts: [],
699
+ errors: [],
700
+ targetPaths: [],
701
+ fileMapping: {}
702
+ };
703
+ logger.info(`Installing ${packageName} directly for ${platform} (no transformations)`);
704
+ try {
705
+ // Copy files AS-IS from package to workspace
706
+ for await (const sourcePath of walkFiles(packageRoot)) {
707
+ const relativePath = relative(packageRoot, sourcePath);
708
+ // Skip metadata files
709
+ if (relativePath.startsWith('.openpackage/') || relativePath === 'openpackage.yml') {
710
+ continue;
711
+ }
712
+ const targetPath = join(workspaceRoot, relativePath);
713
+ result.filesProcessed++;
714
+ if (!dryRun) {
715
+ await ensureDir(dirname(targetPath));
716
+ await fs.copyFile(sourcePath, targetPath);
717
+ result.filesWritten++;
718
+ }
719
+ result.targetPaths.push(targetPath);
720
+ // Track file mapping for uninstall
721
+ if (!result.fileMapping[relativePath]) {
722
+ result.fileMapping[relativePath] = [];
723
+ }
724
+ result.fileMapping[relativePath].push(relativePath);
725
+ }
726
+ logger.info(`Direct installation complete: ${result.filesProcessed} files processed`);
727
+ }
728
+ catch (error) {
729
+ logger.error('Direct installation failed', { packageName, error });
730
+ result.success = false;
731
+ result.errors.push({
732
+ flow: { from: packageRoot, to: workspaceRoot },
733
+ sourcePath: packageRoot,
734
+ error: error,
735
+ message: `Failed to install directly: ${error.message}`
736
+ });
737
+ }
738
+ return result;
739
+ }
740
+ /**
741
+ * Install package with path mapping only (no content transformations)
742
+ *
743
+ * Used for native format packages where content is already correct for the target
744
+ * platform (e.g., Claude plugin with Claude-format frontmatter), but file paths
745
+ * need to be mapped from universal subdirs to platform subdirs.
746
+ *
747
+ * Strategy:
748
+ * 1. Get platform flows for target platform
749
+ * 2. Strip all content transformations (map, pipe operations)
750
+ * 3. Execute flows with path mapping only
751
+ *
752
+ * Example:
753
+ * Source: commands/test.md (Claude plugin root)
754
+ * Target: .claude/commands/test.md (workspace)
755
+ * Content: Unchanged (already in Claude format)
756
+ */
757
+ async function installWithPathMappingOnly(installContext, packageFormat, options) {
758
+ const { packageName, packageRoot, workspaceRoot, platform, packageVersion, priority, dryRun } = installContext;
759
+ const result = {
760
+ success: true,
761
+ filesProcessed: 0,
762
+ filesWritten: 0,
763
+ conflicts: [],
764
+ errors: [],
765
+ targetPaths: [],
766
+ fileMapping: {}
767
+ };
768
+ logger.info(`Installing ${packageName} with path mapping only for ${platform} (native format)`);
769
+ try {
770
+ // Check if platform uses flows
771
+ if (!platformUsesFlows(platform, workspaceRoot)) {
772
+ logger.warn(`Platform ${platform} does not use flows, falling back to direct installation`);
773
+ return await installDirectly(installContext, packageFormat);
774
+ }
775
+ // Get platform flows
776
+ let flows = getApplicableFlows(platform, workspaceRoot);
777
+ if (flows.length === 0) {
778
+ logger.warn(`No flows defined for platform ${platform}, falling back to direct installation`);
779
+ return await installDirectly(installContext, packageFormat);
780
+ }
781
+ // Strip content transformations, keeping only path mappings
782
+ flows = stripContentTransformations(flows);
783
+ logger.debug(`Using ${flows.length} path-mapping-only flows for ${platform}`);
784
+ // Create flow executor
785
+ const executor = createFlowExecutor();
786
+ // Get platform definition
787
+ const platformDef = getPlatformDefinition(platform, workspaceRoot);
788
+ // Build flow context
789
+ const flowContext = {
790
+ workspaceRoot,
791
+ packageRoot,
792
+ platform,
793
+ packageName,
794
+ direction: 'install',
795
+ variables: {
796
+ name: packageName,
797
+ version: packageVersion,
798
+ priority,
799
+ rootFile: platformDef.rootFile,
800
+ rootDir: platformDef.rootDir
801
+ },
802
+ dryRun
803
+ };
804
+ // Discover source files for each flow
805
+ const flowSources = await discoverFlowSources(flows, packageRoot, flowContext);
806
+ // Build override map for platform-specific files
807
+ const overridesByBasePath = new Map();
808
+ for (const [flow, sources] of flowSources) {
809
+ for (const sourceRel of sources) {
810
+ const parsed = parseUniversalPath(sourceRel, { allowPlatformSuffix: true });
811
+ const platformSuffix = parsed?.platformSuffix || extractPlatformSuffixFromFilename(sourceRel);
812
+ if (platformSuffix) {
813
+ const baseKey = parsed
814
+ ? `${parsed.universalSubdir}/${parsed.relPath}`
815
+ : stripPlatformSuffixFromFilename(sourceRel);
816
+ if (!overridesByBasePath.has(baseKey)) {
817
+ overridesByBasePath.set(baseKey, new Set());
818
+ }
819
+ overridesByBasePath.get(baseKey).add(platformSuffix);
820
+ }
821
+ }
822
+ }
823
+ // Execute flows per source file
824
+ for (const [flow, sources] of flowSources) {
825
+ for (const sourceRel of sources) {
826
+ const sourceAbs = join(packageRoot, sourceRel);
827
+ // Check for platform-specific file suffix
828
+ const parsed = parseUniversalPath(sourceRel, { allowPlatformSuffix: true });
829
+ const platformSuffix = parsed?.platformSuffix || extractPlatformSuffixFromFilename(sourceRel);
830
+ const isUniversalSubdirFile = parsed !== null;
831
+ // Skip files not meant for this platform
832
+ if (platformSuffix) {
833
+ const filePlatform = platformSuffix;
834
+ if (filePlatform !== platform) {
835
+ logger.debug(`Skipping ${sourceRel} for platform ${platform} (file is for ${filePlatform})`);
836
+ continue;
837
+ }
838
+ }
839
+ else if (isUniversalSubdirFile && parsed) {
840
+ const baseKey = `${parsed.universalSubdir}/${parsed.relPath}`;
841
+ const overridePlatforms = overridesByBasePath.get(baseKey);
842
+ if (overridePlatforms && overridePlatforms.has(platform)) {
843
+ logger.debug(`Skipping universal file ${sourceRel} for platform ${platform} (overridden by platform-specific file)`);
844
+ continue;
845
+ }
846
+ }
847
+ else {
848
+ const strippedFileName = stripPlatformSuffixFromFilename(sourceRel);
849
+ const hasOverrideForPlatform = sources.some(s => {
850
+ const sSuffix = extractPlatformSuffixFromFilename(s);
851
+ const sStripped = stripPlatformSuffixFromFilename(s);
852
+ return sSuffix === platform && sStripped === strippedFileName;
853
+ });
854
+ if (hasOverrideForPlatform) {
855
+ logger.debug(`Skipping universal file ${sourceRel} for platform ${platform} (overridden by platform-specific file)`);
856
+ continue;
857
+ }
858
+ }
859
+ try {
860
+ // Use suffix-stripped path for flow pattern matching
861
+ const sourceRelForMapping = parsed ? `${parsed.universalSubdir}/${parsed.relPath}` : sourceRel;
862
+ const sourceAbsForMapping = parsed ? join(packageRoot, sourceRelForMapping) : sourceAbs;
863
+ const firstPattern = getFirstFromPattern(flow.from);
864
+ const capturedName = extractCapturedName(sourceRelForMapping, firstPattern);
865
+ const sourceContext = {
866
+ ...flowContext,
867
+ variables: {
868
+ ...flowContext.variables,
869
+ sourcePath: sourceRelForMapping,
870
+ sourceDir: dirname(sourceRelForMapping),
871
+ sourceFile: basename(sourceRelForMapping),
872
+ ...(capturedName ? { capturedName } : {})
873
+ }
874
+ };
875
+ // Resolve target path
876
+ const rawToPattern = typeof flow.to === 'string' ? flow.to : Object.keys(flow.to)[0] ?? '';
877
+ const resolvedToPattern = resolvePattern(rawToPattern, sourceContext, capturedName);
878
+ const targetAbs = resolveTargetFromGlob(sourceAbsForMapping, firstPattern, resolvedToPattern, sourceContext);
879
+ const targetRel = relative(workspaceRoot, targetAbs);
880
+ // Create concrete flow
881
+ const concreteFlow = {
882
+ ...flow,
883
+ from: sourceRel,
884
+ to: targetRel
885
+ };
886
+ const flowResult = await executor.executeFlow(concreteFlow, sourceContext);
887
+ const wasSkipped = flowResult.warnings?.includes('Flow skipped due to condition');
888
+ if (!wasSkipped) {
889
+ result.filesProcessed++;
890
+ }
891
+ if (flowResult.success && !wasSkipped) {
892
+ const target = typeof flowResult.target === 'string' ? flowResult.target : flowResult.target;
893
+ if (typeof target === 'string') {
894
+ result.targetPaths.push(target);
895
+ const targetRelFromWorkspace = relative(workspaceRoot, target);
896
+ if (!result.fileMapping[sourceRel])
897
+ result.fileMapping[sourceRel] = [];
898
+ const normalizedTargetRel = targetRelFromWorkspace.replace(/\\/g, '/');
899
+ const isKeyTrackedMerge = (flowResult.merge === 'deep' || flowResult.merge === 'shallow') &&
900
+ Array.isArray(flowResult.keys);
901
+ if (isKeyTrackedMerge) {
902
+ result.fileMapping[sourceRel].push({
903
+ target: normalizedTargetRel,
904
+ merge: flowResult.merge,
905
+ keys: flowResult.keys
906
+ });
907
+ }
908
+ else {
909
+ result.fileMapping[sourceRel].push(normalizedTargetRel);
910
+ }
911
+ }
912
+ if (!dryRun) {
913
+ result.filesWritten++;
914
+ }
915
+ if (flowResult.conflicts && flowResult.conflicts.length > 0) {
916
+ for (const conflict of flowResult.conflicts) {
917
+ const packages = [];
918
+ packages.push({ packageName: conflict.winner, priority: 0, chosen: true });
919
+ for (const loser of conflict.losers) {
920
+ packages.push({ packageName: loser, priority: 0, chosen: false });
921
+ }
922
+ result.conflicts.push({
923
+ targetPath: conflict.path,
924
+ packages,
925
+ message: `Conflict in ${conflict.path}: ${conflict.winner} overwrites ${conflict.losers.join(', ')}`
926
+ });
927
+ }
928
+ }
929
+ }
930
+ else if (!flowResult.success) {
931
+ result.success = false;
932
+ result.errors.push({
933
+ flow,
934
+ sourcePath: sourceRel,
935
+ error: flowResult.error || new Error('Unknown error'),
936
+ message: `Failed to execute flow for ${sourceRel}: ${flowResult.error?.message || 'Unknown error'}`
937
+ });
938
+ }
939
+ }
940
+ catch (error) {
941
+ result.success = false;
942
+ result.errors.push({
943
+ flow,
944
+ sourcePath: sourceRel,
945
+ error: error,
946
+ message: `Error processing ${sourceRel}: ${error.message}`
947
+ });
948
+ }
949
+ }
950
+ }
951
+ // Log results
952
+ if (result.filesProcessed > 0) {
953
+ logger.info(`Processed ${result.filesProcessed} files for ${packageName} on platform ${platform}` +
954
+ (dryRun ? ' (dry run)' : `, wrote ${result.filesWritten} files`));
955
+ }
956
+ // Log conflicts
957
+ if (result.conflicts.length > 0) {
958
+ logger.warn(`Detected ${result.conflicts.length} conflicts during installation`);
959
+ for (const conflict of result.conflicts) {
960
+ const winner = conflict.packages.find(p => p.chosen);
961
+ logger.warn(` ${toTildePath(conflict.targetPath)}: ${winner?.packageName} (priority ${winner?.priority}) overwrites ` +
962
+ `${conflict.packages.find(p => !p.chosen)?.packageName}`);
963
+ }
964
+ }
965
+ // Log errors
966
+ if (result.errors.length > 0) {
967
+ logger.error(`Encountered ${result.errors.length} errors during installation`);
968
+ for (const error of result.errors) {
969
+ logger.error(` ${error.sourcePath}: ${error.message}`);
970
+ }
971
+ }
972
+ }
973
+ catch (error) {
974
+ result.success = false;
975
+ logger.error(`Failed to install package ${packageName} with path mapping: ${error.message}`);
976
+ result.errors.push({
977
+ flow: { from: packageRoot, to: workspaceRoot },
978
+ sourcePath: packageRoot,
979
+ error: error,
980
+ message: `Failed to install with path mapping: ${error.message}`
981
+ });
982
+ }
983
+ return result;
984
+ }
985
+ /**
986
+ * Strip content transformations from flows, keeping only path mappings
987
+ *
988
+ * Removes:
989
+ * - map operations (frontmatter transformations)
990
+ * - pipe operations (except format converters needed for file type changes)
991
+ *
992
+ * Keeps:
993
+ * - from/to path patterns (the core path mapping)
994
+ * - merge strategies (for multi-package composition)
995
+ * - when conditions (for conditional flows)
996
+ */
997
+ function stripContentTransformations(flows) {
998
+ return flows.map(flow => {
999
+ const strippedFlow = {
1000
+ from: flow.from,
1001
+ to: flow.to
1002
+ };
1003
+ // Keep merge strategy if defined
1004
+ if (flow.merge) {
1005
+ strippedFlow.merge = flow.merge;
1006
+ }
1007
+ // Keep when conditions
1008
+ if (flow.when) {
1009
+ strippedFlow.when = flow.when;
1010
+ }
1011
+ // Note: pipe transforms are now handled within the map pipeline via $pipe operation
1012
+ // Explicitly skip map transformations (commented for clarity)
1013
+ // strippedFlow.map = undefined;
1014
+ return strippedFlow;
1015
+ });
1016
+ }
1017
+ /**
1018
+ * Install package with format conversion
1019
+ * Converts from source platform format → universal → target platform format
1020
+ */
1021
+ async function installWithConversion(installContext, packageFormat, options) {
1022
+ const { packageName, packageRoot, workspaceRoot, platform, dryRun } = installContext;
1023
+ const result = {
1024
+ success: true,
1025
+ filesProcessed: 0,
1026
+ filesWritten: 0,
1027
+ conflicts: [],
1028
+ errors: [],
1029
+ targetPaths: [],
1030
+ fileMapping: {}
1031
+ };
1032
+ try {
1033
+ // Step 1: Load package files
1034
+ const { readTextFile } = await import('../../utils/fs.js');
1035
+ const packageFiles = [];
1036
+ for await (const sourcePath of walkFiles(packageRoot)) {
1037
+ const relativePath = relative(packageRoot, sourcePath);
1038
+ // Skip metadata
1039
+ if (relativePath.startsWith('.openpackage/') || relativePath === 'openpackage.yml') {
1040
+ continue;
1041
+ }
1042
+ const content = await readTextFile(sourcePath);
1043
+ packageFiles.push({ path: relativePath, content, encoding: 'utf8' });
1044
+ }
1045
+ // Step 2: Create package object
1046
+ const pkg = {
1047
+ metadata: {
1048
+ name: packageName,
1049
+ version: installContext.packageVersion
1050
+ },
1051
+ files: packageFiles,
1052
+ _format: packageFormat
1053
+ };
1054
+ // Step 3: Convert from source platform format to universal format
1055
+ const converter = createPlatformConverter(workspaceRoot);
1056
+ const conversionResult = await converter.convert(pkg, platform, { dryRun });
1057
+ if (!conversionResult.success || !conversionResult.convertedPackage) {
1058
+ logger.error('Package conversion failed', {
1059
+ package: packageName,
1060
+ stages: conversionResult.stages
1061
+ });
1062
+ result.success = false;
1063
+ result.errors.push({
1064
+ flow: { from: packageRoot, to: workspaceRoot },
1065
+ sourcePath: packageRoot,
1066
+ error: new Error('Conversion failed'),
1067
+ message: 'Failed to convert package format'
1068
+ });
1069
+ return result;
1070
+ }
1071
+ logger.info(`Conversion to universal format complete (${conversionResult.stages.length} stages), now applying ${platform} platform flows`);
1072
+ // Step 4: Write converted (universal format) files to temporary directory
1073
+ const { mkdtemp, rm } = await import('fs/promises');
1074
+ const { tmpdir } = await import('os');
1075
+ const { writeTextFile, ensureDir: ensureDirUtil } = await import('../../utils/fs.js');
1076
+ let tempPackageRoot = null;
1077
+ try {
1078
+ tempPackageRoot = await mkdtemp(join(tmpdir(), 'opkg-converted-'));
1079
+ // Write all converted files to temp directory
1080
+ for (const file of conversionResult.convertedPackage.files) {
1081
+ const filePath = join(tempPackageRoot, file.path);
1082
+ await ensureDirUtil(dirname(filePath));
1083
+ await writeTextFile(filePath, file.content);
1084
+ }
1085
+ logger.debug(`Wrote ${conversionResult.convertedPackage.files.length} converted files to temp directory`, {
1086
+ tempPackageRoot
1087
+ });
1088
+ // Step 5: Install from temp directory using standard flow-based installation
1089
+ // This will apply the target platform flows to the now-universal-format content
1090
+ const convertedInstallContext = {
1091
+ ...installContext,
1092
+ packageRoot: tempPackageRoot,
1093
+ // Important: Clear packageFormat so it gets re-detected as universal format
1094
+ packageFormat: undefined
1095
+ };
1096
+ // Recursively call installPackageWithFlows, but with converted package root
1097
+ // This will apply standard platform flows (Universal → Target Platform)
1098
+ const installResult = await installPackageWithFlows(convertedInstallContext, options);
1099
+ // Cleanup temp directory
1100
+ if (tempPackageRoot) {
1101
+ await rm(tempPackageRoot, { recursive: true, force: true });
1102
+ }
1103
+ return installResult;
1104
+ }
1105
+ catch (error) {
1106
+ // Cleanup on error
1107
+ if (tempPackageRoot) {
1108
+ try {
1109
+ await rm(tempPackageRoot, { recursive: true, force: true });
1110
+ }
1111
+ catch (cleanupError) {
1112
+ logger.warn('Failed to cleanup temp directory after error', { tempPackageRoot, cleanupError });
1113
+ }
1114
+ }
1115
+ logger.error('Failed to install converted package', { packageName, error });
1116
+ result.success = false;
1117
+ result.errors.push({
1118
+ flow: { from: packageRoot, to: workspaceRoot },
1119
+ sourcePath: packageRoot,
1120
+ error: error,
1121
+ message: `Failed to install converted package: ${error.message}`
1122
+ });
1123
+ return result;
1124
+ }
1125
+ }
1126
+ catch (error) {
1127
+ logger.error('Conversion installation failed', { packageName, error });
1128
+ result.success = false;
1129
+ result.errors.push({
1130
+ flow: { from: packageRoot, to: workspaceRoot },
1131
+ sourcePath: packageRoot,
1132
+ error: error,
1133
+ message: `Failed to install with conversion: ${error.message}`
1134
+ });
1135
+ return result;
1136
+ }
1137
+ }
1138
+ // ============================================================================
1139
+ // Helper Functions
1140
+ // ============================================================================
1141
+ /**
1142
+ * Check if a file should be processed with flows
1143
+ */
1144
+ export function shouldUseFlows(platform, cwd) {
1145
+ return platformUsesFlows(platform, cwd);
1146
+ }
1147
+ /**
1148
+ * Get flow statistics for reporting
1149
+ */
1150
+ export function getFlowStatistics(result) {
1151
+ return {
1152
+ total: result.filesProcessed,
1153
+ written: result.filesWritten,
1154
+ conflicts: result.conflicts.length,
1155
+ errors: result.errors.length
1156
+ };
1157
+ }
1158
+ //# sourceMappingURL=flow-based-installer.js.map