mnenv 0.1.0 → 0.1.2

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 (306) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +3 -0
  3. data/PROPOSAL.md +197 -0
  4. data/README.adoc +168 -461
  5. data/Rakefile +7 -4
  6. data/bin/Install-Mnenv.ps1 +145 -0
  7. data/bin/mnenv-installer +72 -0
  8. data/completions/bash +47 -0
  9. data/completions/fish +29 -0
  10. data/completions/powershell.ps1 +94 -0
  11. data/completions/zsh +43 -0
  12. data/lib/mnenv/binary_repository.rb +189 -0
  13. data/lib/mnenv/chocolatey.rb +7 -0
  14. data/lib/mnenv/cli.rb +110 -10
  15. data/lib/mnenv/commands/available_command.rb +169 -0
  16. data/lib/mnenv/commands/chocolatey_command.rb +4 -5
  17. data/lib/mnenv/commands/gemfile_command.rb +4 -5
  18. data/lib/mnenv/commands/homebrew_command.rb +4 -5
  19. data/lib/mnenv/commands/install_command.rb +234 -0
  20. data/lib/mnenv/commands/snap_command.rb +5 -7
  21. data/lib/mnenv/commands/uninstall_command.rb +111 -0
  22. data/lib/mnenv/commands/version_command.rb +167 -0
  23. data/lib/mnenv/commands.rb +9 -4
  24. data/lib/mnenv/gemfile/extractor.rb +10 -3
  25. data/lib/mnenv/gemfile.rb +8 -0
  26. data/lib/mnenv/gemfile_repository.rb +0 -2
  27. data/lib/mnenv/homebrew.rb +7 -0
  28. data/lib/mnenv/installer/base.rb +62 -0
  29. data/lib/mnenv/installer/factory.rb +46 -0
  30. data/lib/mnenv/installer.rb +12 -0
  31. data/lib/mnenv/installers/binary_installer.rb +242 -0
  32. data/lib/mnenv/installers/gemfile_installer.rb +76 -0
  33. data/lib/mnenv/json_formatter.rb +3 -13
  34. data/lib/mnenv/logger.rb +9 -1
  35. data/lib/mnenv/models/binary_version.rb +78 -0
  36. data/lib/mnenv/models/chocolatey_version.rb +7 -0
  37. data/lib/mnenv/models/gemfile_version.rb +19 -5
  38. data/lib/mnenv/models/homebrew_version.rb +7 -0
  39. data/lib/mnenv/models/snap_version.rb +8 -0
  40. data/lib/mnenv/models/version.rb +16 -0
  41. data/lib/mnenv/models.rb +7 -5
  42. data/lib/mnenv/paths.rb +69 -0
  43. data/lib/mnenv/platform_detector.rb +109 -0
  44. data/lib/mnenv/repository.rb +50 -35
  45. data/lib/mnenv/resolver +72 -0
  46. data/lib/mnenv/shells/base.rb +32 -0
  47. data/lib/mnenv/shells/bash.rb +72 -0
  48. data/lib/mnenv/shells/cmd.rb +108 -0
  49. data/lib/mnenv/shells/factory.rb +82 -0
  50. data/lib/mnenv/shells/power_shell.rb +110 -0
  51. data/lib/mnenv/shim_manager.rb +121 -0
  52. data/lib/mnenv/snap.rb +7 -0
  53. data/lib/mnenv/snap_repository.rb +2 -19
  54. data/lib/mnenv/source_registry.rb +69 -0
  55. data/lib/mnenv/sources.rb +46 -0
  56. data/lib/mnenv/version.rb +1 -1
  57. data/lib/mnenv/version_resolver.rb +108 -0
  58. data/lib/mnenv/versions_manager.rb +92 -0
  59. data/lib/mnenv.rb +6 -0
  60. data/mnenv.gemspec +4 -1
  61. data/scripts/cross-source-switching-test.sh +214 -0
  62. data/scripts/integration-test.sh +89 -0
  63. data/scripts/version-switching-test.sh +151 -0
  64. metadata +85 -247
  65. data/data/chocolatey/versions.yaml +0 -812
  66. data/data/gemfile/v1.1.6/Gemfile +0 -4
  67. data/data/gemfile/v1.1.6/Gemfile.lock.archived +0 -232
  68. data/data/gemfile/v1.1.7/Gemfile +0 -4
  69. data/data/gemfile/v1.1.7/Gemfile.lock.archived +0 -235
  70. data/data/gemfile/v1.1.8/Gemfile +0 -4
  71. data/data/gemfile/v1.1.8/Gemfile.lock.archived +0 -238
  72. data/data/gemfile/v1.10.0/Gemfile +0 -5
  73. data/data/gemfile/v1.10.0/Gemfile.lock.archived +0 -930
  74. data/data/gemfile/v1.10.1/Gemfile +0 -5
  75. data/data/gemfile/v1.10.1/Gemfile.lock.archived +0 -929
  76. data/data/gemfile/v1.10.10/Gemfile +0 -5
  77. data/data/gemfile/v1.10.10/Gemfile.lock.archived +0 -973
  78. data/data/gemfile/v1.10.11/Gemfile +0 -5
  79. data/data/gemfile/v1.10.11/Gemfile.lock.archived +0 -975
  80. data/data/gemfile/v1.10.2/Gemfile +0 -5
  81. data/data/gemfile/v1.10.2/Gemfile.lock.archived +0 -939
  82. data/data/gemfile/v1.10.3/Gemfile +0 -5
  83. data/data/gemfile/v1.10.3/Gemfile.lock.archived +0 -946
  84. data/data/gemfile/v1.10.5/Gemfile +0 -5
  85. data/data/gemfile/v1.10.5/Gemfile.lock.archived +0 -958
  86. data/data/gemfile/v1.10.6/Gemfile +0 -5
  87. data/data/gemfile/v1.10.6/Gemfile.lock.archived +0 -969
  88. data/data/gemfile/v1.10.7/Gemfile +0 -5
  89. data/data/gemfile/v1.10.7/Gemfile.lock.archived +0 -969
  90. data/data/gemfile/v1.10.8/Gemfile +0 -5
  91. data/data/gemfile/v1.10.8/Gemfile.lock.archived +0 -968
  92. data/data/gemfile/v1.10.9/Gemfile +0 -5
  93. data/data/gemfile/v1.10.9/Gemfile.lock.archived +0 -972
  94. data/data/gemfile/v1.11.0/Gemfile +0 -5
  95. data/data/gemfile/v1.11.0/Gemfile.lock.archived +0 -971
  96. data/data/gemfile/v1.11.1/Gemfile +0 -5
  97. data/data/gemfile/v1.11.1/Gemfile.lock.archived +0 -975
  98. data/data/gemfile/v1.11.4/Gemfile +0 -5
  99. data/data/gemfile/v1.11.4/Gemfile.lock.archived +0 -1046
  100. data/data/gemfile/v1.11.5/Gemfile +0 -5
  101. data/data/gemfile/v1.11.5/Gemfile.lock.archived +0 -1047
  102. data/data/gemfile/v1.12.10/Gemfile +0 -3
  103. data/data/gemfile/v1.12.10/Gemfile.lock.archived +0 -1073
  104. data/data/gemfile/v1.12.3/Gemfile +0 -3
  105. data/data/gemfile/v1.12.3/Gemfile.lock.archived +0 -1050
  106. data/data/gemfile/v1.12.4/Gemfile +0 -3
  107. data/data/gemfile/v1.12.4/Gemfile.lock.archived +0 -1056
  108. data/data/gemfile/v1.12.5/Gemfile +0 -3
  109. data/data/gemfile/v1.12.5/Gemfile.lock.archived +0 -1054
  110. data/data/gemfile/v1.12.6/Gemfile +0 -3
  111. data/data/gemfile/v1.12.6/Gemfile.lock.archived +0 -1056
  112. data/data/gemfile/v1.12.8/Gemfile +0 -3
  113. data/data/gemfile/v1.12.8/Gemfile.lock.archived +0 -1063
  114. data/data/gemfile/v1.13.0/Gemfile +0 -3
  115. data/data/gemfile/v1.13.0/Gemfile.lock.archived +0 -1074
  116. data/data/gemfile/v1.13.2/Gemfile +0 -3
  117. data/data/gemfile/v1.13.2/Gemfile.lock.archived +0 -899
  118. data/data/gemfile/v1.13.3/Gemfile +0 -3
  119. data/data/gemfile/v1.13.3/Gemfile.lock.archived +0 -938
  120. data/data/gemfile/v1.13.4/Gemfile +0 -3
  121. data/data/gemfile/v1.13.4/Gemfile.lock.archived +0 -938
  122. data/data/gemfile/v1.13.5/Gemfile +0 -3
  123. data/data/gemfile/v1.13.5/Gemfile.lock.archived +0 -944
  124. data/data/gemfile/v1.13.7/Gemfile +0 -3
  125. data/data/gemfile/v1.13.7/Gemfile.lock.archived +0 -944
  126. data/data/gemfile/v1.13.8/Gemfile +0 -3
  127. data/data/gemfile/v1.13.8/Gemfile.lock.archived +0 -944
  128. data/data/gemfile/v1.13.9/Gemfile +0 -3
  129. data/data/gemfile/v1.13.9/Gemfile.lock.archived +0 -956
  130. data/data/gemfile/v1.14.3/Gemfile +0 -3
  131. data/data/gemfile/v1.14.3/Gemfile.lock.archived +0 -950
  132. data/data/gemfile/v1.2.12/Gemfile +0 -3
  133. data/data/gemfile/v1.2.12/Gemfile.lock.archived +0 -283
  134. data/data/gemfile/v1.2.2/Gemfile +0 -4
  135. data/data/gemfile/v1.2.2/Gemfile.lock.archived +0 -224
  136. data/data/gemfile/v1.2.3/Gemfile +0 -4
  137. data/data/gemfile/v1.2.3/Gemfile.lock.archived +0 -231
  138. data/data/gemfile/v1.2.6/Gemfile +0 -4
  139. data/data/gemfile/v1.2.6/Gemfile.lock.archived +0 -239
  140. data/data/gemfile/v1.2.8/Gemfile +0 -4
  141. data/data/gemfile/v1.2.8/Gemfile.lock.archived +0 -233
  142. data/data/gemfile/v1.2.9/Gemfile +0 -4
  143. data/data/gemfile/v1.2.9/Gemfile.lock.archived +0 -245
  144. data/data/gemfile/v1.3.1/Gemfile +0 -3
  145. data/data/gemfile/v1.3.1/Gemfile.lock.archived +0 -296
  146. data/data/gemfile/v1.3.2/Gemfile +0 -3
  147. data/data/gemfile/v1.3.2/Gemfile.lock.archived +0 -296
  148. data/data/gemfile/v1.3.4/Gemfile +0 -3
  149. data/data/gemfile/v1.3.4/Gemfile.lock.archived +0 -284
  150. data/data/gemfile/v1.3.5/Gemfile +0 -3
  151. data/data/gemfile/v1.3.5/Gemfile.lock.archived +0 -284
  152. data/data/gemfile/v1.3.6/Gemfile +0 -3
  153. data/data/gemfile/v1.3.6/Gemfile.lock.archived +0 -286
  154. data/data/gemfile/v1.3.9/Gemfile +0 -3
  155. data/data/gemfile/v1.3.9/Gemfile.lock.archived +0 -334
  156. data/data/gemfile/v1.4.0/Gemfile +0 -3
  157. data/data/gemfile/v1.4.0/Gemfile.lock.archived +0 -330
  158. data/data/gemfile/v1.4.10/Gemfile +0 -4
  159. data/data/gemfile/v1.4.10/Gemfile.lock.archived +0 -461
  160. data/data/gemfile/v1.4.11/Gemfile +0 -4
  161. data/data/gemfile/v1.4.11/Gemfile.lock.archived +0 -452
  162. data/data/gemfile/v1.4.12/Gemfile +0 -4
  163. data/data/gemfile/v1.4.12/Gemfile.lock.archived +0 -452
  164. data/data/gemfile/v1.4.13/Gemfile +0 -4
  165. data/data/gemfile/v1.4.13/Gemfile.lock.archived +0 -455
  166. data/data/gemfile/v1.4.14/Gemfile +0 -4
  167. data/data/gemfile/v1.4.14/Gemfile.lock.archived +0 -456
  168. data/data/gemfile/v1.4.18/Gemfile +0 -3
  169. data/data/gemfile/v1.4.18/Gemfile.lock.archived +0 -486
  170. data/data/gemfile/v1.4.3/Gemfile +0 -3
  171. data/data/gemfile/v1.4.3/Gemfile.lock.archived +0 -339
  172. data/data/gemfile/v1.4.4/Gemfile +0 -3
  173. data/data/gemfile/v1.4.4/Gemfile.lock.archived +0 -339
  174. data/data/gemfile/v1.4.5/Gemfile +0 -3
  175. data/data/gemfile/v1.4.5/Gemfile.lock.archived +0 -348
  176. data/data/gemfile/v1.4.6/Gemfile +0 -3
  177. data/data/gemfile/v1.4.6/Gemfile.lock.archived +0 -357
  178. data/data/gemfile/v1.4.7/Gemfile +0 -3
  179. data/data/gemfile/v1.4.7/Gemfile.lock.archived +0 -391
  180. data/data/gemfile/v1.4.8/Gemfile +0 -3
  181. data/data/gemfile/v1.4.8/Gemfile.lock.archived +0 -445
  182. data/data/gemfile/v1.4.9/Gemfile +0 -3
  183. data/data/gemfile/v1.4.9/Gemfile.lock.archived +0 -448
  184. data/data/gemfile/v1.5.0/Gemfile +0 -3
  185. data/data/gemfile/v1.5.0/Gemfile.lock.archived +0 -478
  186. data/data/gemfile/v1.5.10/Gemfile +0 -3
  187. data/data/gemfile/v1.5.10/Gemfile.lock.archived +0 -668
  188. data/data/gemfile/v1.5.11/Gemfile +0 -3
  189. data/data/gemfile/v1.5.11/Gemfile.lock.archived +0 -668
  190. data/data/gemfile/v1.5.15/Gemfile +0 -3
  191. data/data/gemfile/v1.5.15/Gemfile.lock.archived +0 -686
  192. data/data/gemfile/v1.5.16/Gemfile +0 -3
  193. data/data/gemfile/v1.5.16/Gemfile.lock.archived +0 -684
  194. data/data/gemfile/v1.5.17/Gemfile +0 -3
  195. data/data/gemfile/v1.5.17/Gemfile.lock.archived +0 -684
  196. data/data/gemfile/v1.5.18/Gemfile +0 -5
  197. data/data/gemfile/v1.5.18/Gemfile.lock.archived +0 -691
  198. data/data/gemfile/v1.5.19/Gemfile +0 -5
  199. data/data/gemfile/v1.5.19/Gemfile.lock.archived +0 -703
  200. data/data/gemfile/v1.5.20/Gemfile +0 -5
  201. data/data/gemfile/v1.5.20/Gemfile.lock.archived +0 -703
  202. data/data/gemfile/v1.5.21/Gemfile +0 -5
  203. data/data/gemfile/v1.5.21/Gemfile.lock.archived +0 -707
  204. data/data/gemfile/v1.5.22/Gemfile +0 -5
  205. data/data/gemfile/v1.5.22/Gemfile.lock.archived +0 -707
  206. data/data/gemfile/v1.5.23/Gemfile +0 -5
  207. data/data/gemfile/v1.5.23/Gemfile.lock.archived +0 -711
  208. data/data/gemfile/v1.5.24/Gemfile +0 -5
  209. data/data/gemfile/v1.5.24/Gemfile.lock.archived +0 -711
  210. data/data/gemfile/v1.5.3/Gemfile +0 -3
  211. data/data/gemfile/v1.5.3/Gemfile.lock.archived +0 -651
  212. data/data/gemfile/v1.5.4/Gemfile +0 -3
  213. data/data/gemfile/v1.5.4/Gemfile.lock.archived +0 -657
  214. data/data/gemfile/v1.5.5/Gemfile +0 -3
  215. data/data/gemfile/v1.5.5/Gemfile.lock.archived +0 -657
  216. data/data/gemfile/v1.5.6/Gemfile +0 -3
  217. data/data/gemfile/v1.5.6/Gemfile.lock.archived +0 -657
  218. data/data/gemfile/v1.5.7/Gemfile +0 -3
  219. data/data/gemfile/v1.5.7/Gemfile.lock.archived +0 -657
  220. data/data/gemfile/v1.5.8/Gemfile +0 -3
  221. data/data/gemfile/v1.5.8/Gemfile.lock.archived +0 -655
  222. data/data/gemfile/v1.5.9/Gemfile +0 -3
  223. data/data/gemfile/v1.5.9/Gemfile.lock.archived +0 -656
  224. data/data/gemfile/v1.6.1/Gemfile +0 -5
  225. data/data/gemfile/v1.6.1/Gemfile.lock.archived +0 -721
  226. data/data/gemfile/v1.6.10/Gemfile +0 -5
  227. data/data/gemfile/v1.6.10/Gemfile.lock.archived +0 -744
  228. data/data/gemfile/v1.6.11/Gemfile +0 -5
  229. data/data/gemfile/v1.6.11/Gemfile.lock.archived +0 -744
  230. data/data/gemfile/v1.6.12/Gemfile +0 -5
  231. data/data/gemfile/v1.6.12/Gemfile.lock.archived +0 -745
  232. data/data/gemfile/v1.6.13/Gemfile +0 -5
  233. data/data/gemfile/v1.6.13/Gemfile.lock.archived +0 -745
  234. data/data/gemfile/v1.6.14/Gemfile +0 -5
  235. data/data/gemfile/v1.6.14/Gemfile.lock.archived +0 -754
  236. data/data/gemfile/v1.6.15/Gemfile +0 -5
  237. data/data/gemfile/v1.6.15/Gemfile.lock.archived +0 -757
  238. data/data/gemfile/v1.6.2/Gemfile +0 -5
  239. data/data/gemfile/v1.6.2/Gemfile.lock.archived +0 -718
  240. data/data/gemfile/v1.6.3/Gemfile +0 -5
  241. data/data/gemfile/v1.6.3/Gemfile.lock.archived +0 -728
  242. data/data/gemfile/v1.6.4/Gemfile +0 -5
  243. data/data/gemfile/v1.6.4/Gemfile.lock.archived +0 -730
  244. data/data/gemfile/v1.6.5/Gemfile +0 -5
  245. data/data/gemfile/v1.6.5/Gemfile.lock.archived +0 -733
  246. data/data/gemfile/v1.6.6/Gemfile +0 -5
  247. data/data/gemfile/v1.6.6/Gemfile.lock.archived +0 -733
  248. data/data/gemfile/v1.6.7/Gemfile +0 -5
  249. data/data/gemfile/v1.6.7/Gemfile.lock.archived +0 -733
  250. data/data/gemfile/v1.6.9/Gemfile +0 -5
  251. data/data/gemfile/v1.6.9/Gemfile.lock.archived +0 -744
  252. data/data/gemfile/v1.7.0/Gemfile +0 -5
  253. data/data/gemfile/v1.7.0/Gemfile.lock.archived +0 -750
  254. data/data/gemfile/v1.7.1/Gemfile +0 -5
  255. data/data/gemfile/v1.7.1/Gemfile.lock.archived +0 -750
  256. data/data/gemfile/v1.7.2/Gemfile +0 -5
  257. data/data/gemfile/v1.7.2/Gemfile.lock.archived +0 -747
  258. data/data/gemfile/v1.7.3/Gemfile +0 -5
  259. data/data/gemfile/v1.7.3/Gemfile.lock.archived +0 -755
  260. data/data/gemfile/v1.7.4/Gemfile +0 -5
  261. data/data/gemfile/v1.7.4/Gemfile.lock.archived +0 -756
  262. data/data/gemfile/v1.7.5/Gemfile +0 -5
  263. data/data/gemfile/v1.7.5/Gemfile.lock.archived +0 -759
  264. data/data/gemfile/v1.7.6/Gemfile +0 -5
  265. data/data/gemfile/v1.7.6/Gemfile.lock.archived +0 -768
  266. data/data/gemfile/v1.8.10/Gemfile +0 -5
  267. data/data/gemfile/v1.8.10/Gemfile.lock.archived +0 -792
  268. data/data/gemfile/v1.8.11/Gemfile +0 -5
  269. data/data/gemfile/v1.8.11/Gemfile.lock.archived +0 -862
  270. data/data/gemfile/v1.8.3/Gemfile +0 -5
  271. data/data/gemfile/v1.8.3/Gemfile.lock.archived +0 -773
  272. data/data/gemfile/v1.8.4/Gemfile +0 -5
  273. data/data/gemfile/v1.8.4/Gemfile.lock.archived +0 -768
  274. data/data/gemfile/v1.8.5/Gemfile +0 -5
  275. data/data/gemfile/v1.8.5/Gemfile.lock.archived +0 -768
  276. data/data/gemfile/v1.8.6/Gemfile +0 -5
  277. data/data/gemfile/v1.8.6/Gemfile.lock.archived +0 -777
  278. data/data/gemfile/v1.8.7/Gemfile +0 -5
  279. data/data/gemfile/v1.8.7/Gemfile.lock.archived +0 -777
  280. data/data/gemfile/v1.8.8/Gemfile +0 -5
  281. data/data/gemfile/v1.8.8/Gemfile.lock.archived +0 -778
  282. data/data/gemfile/v1.8.9/Gemfile +0 -5
  283. data/data/gemfile/v1.8.9/Gemfile.lock.archived +0 -775
  284. data/data/gemfile/v1.9.0/Gemfile +0 -5
  285. data/data/gemfile/v1.9.0/Gemfile.lock.archived +0 -871
  286. data/data/gemfile/v1.9.1/Gemfile +0 -5
  287. data/data/gemfile/v1.9.1/Gemfile.lock.archived +0 -906
  288. data/data/gemfile/v1.9.2/Gemfile +0 -5
  289. data/data/gemfile/v1.9.2/Gemfile.lock.archived +0 -898
  290. data/data/gemfile/v1.9.3/Gemfile +0 -5
  291. data/data/gemfile/v1.9.3/Gemfile.lock.archived +0 -898
  292. data/data/gemfile/v1.9.4/Gemfile +0 -5
  293. data/data/gemfile/v1.9.4/Gemfile.lock.archived +0 -901
  294. data/data/gemfile/v1.9.5/Gemfile +0 -5
  295. data/data/gemfile/v1.9.5/Gemfile.lock.archived +0 -903
  296. data/data/gemfile/v1.9.6/Gemfile +0 -5
  297. data/data/gemfile/v1.9.6/Gemfile.lock.archived +0 -900
  298. data/data/gemfile/v1.9.7/Gemfile +0 -5
  299. data/data/gemfile/v1.9.7/Gemfile.lock.archived +0 -922
  300. data/data/gemfile/v1.9.8/Gemfile +0 -5
  301. data/data/gemfile/v1.9.8/Gemfile.lock.archived +0 -933
  302. data/data/gemfile/versions.yaml +0 -751
  303. data/data/homebrew/versions.yaml +0 -567
  304. data/data/snap/github_tags.json +0 -42
  305. data/data/snap/versions.yaml +0 -589
  306. data/snapcraft-list-copied-from-site.md +0 -101
data/lib/mnenv/cli.rb CHANGED
@@ -2,18 +2,49 @@
2
2
 
3
3
  require 'thor'
4
4
  require 'json'
5
- require_relative 'models'
6
- require_relative 'commands'
7
- require_relative 'json_formatter'
8
- require_relative 'gemfile_repository'
9
- require_relative 'snap_repository'
10
- require_relative 'homebrew_repository'
11
- require_relative 'chocolatey_repository'
12
5
 
13
6
  module Mnenv
7
+ # Autoload constants for lazy loading
8
+ autoload :VERSION, 'mnenv/version'
9
+ autoload :GemfileRepository, 'mnenv/gemfile_repository'
10
+ autoload :SnapRepository, 'mnenv/snap_repository'
11
+ autoload :HomebrewRepository, 'mnenv/homebrew_repository'
12
+ autoload :ChocolateyRepository, 'mnenv/chocolatey_repository'
13
+ autoload :GemfileCommand, 'mnenv/commands/gemfile_command'
14
+ autoload :SnapCommand, 'mnenv/commands/snap_command'
15
+ autoload :HomebrewCommand, 'mnenv/commands/homebrew_command'
16
+ autoload :ChocolateyCommand, 'mnenv/commands/chocolatey_command'
17
+ autoload :InstallCommand, 'mnenv/commands/install_command'
18
+ autoload :VersionCommand, 'mnenv/commands/version_command'
19
+ autoload :UninstallCommand, 'mnenv/commands/uninstall_command'
20
+ autoload :JsonFormatter, 'mnenv/json_formatter'
21
+ autoload :ShimManager, 'mnenv/shim_manager'
22
+ autoload :BinaryRepository, 'mnenv/binary_repository'
23
+
14
24
  class Cli < Thor
15
25
  package_name 'mnenv'
16
26
 
27
+ # Global options
28
+ class_option :verbose, type: :boolean, default: false, desc: 'Enable verbose output'
29
+ class_option :'data-dir', type: :string, desc: 'Path to data directory (default: uses ~/.mnenv/versions/data)'
30
+
31
+ # Track verbose mode and data_dir for use in other classes
32
+ class << self
33
+ def verbose?
34
+ @verbose ||= false
35
+ end
36
+
37
+ attr_accessor :data_dir
38
+
39
+ attr_writer :verbose
40
+ end
41
+
42
+ def initialize(*args)
43
+ super
44
+ self.class.verbose = options[:verbose]
45
+ self.class.data_dir = options[:'data-dir']
46
+ end
47
+
17
48
  # Platform subcommands
18
49
  desc 'gemfile SUBCOMMAND', 'Manage Ruby (Gemfile) versions'
19
50
  subcommand 'gemfile', GemfileCommand
@@ -27,12 +58,75 @@ module Mnenv
27
58
  desc 'chocolatey SUBCOMMAND', 'Manage Chocolatey versions'
28
59
  subcommand 'chocolatey', ChocolateyCommand
29
60
 
61
+ # Interactive installation commands
62
+ desc 'install [VERSION]', 'Install a Metanorma version'
63
+ method_option :source, type: :string, enum: %w[gemfile binary], default: 'gemfile',
64
+ desc: 'Installation source (gemfile or binary)'
65
+ method_option :interactive, type: :boolean, aliases: '-i', default: false,
66
+ desc: 'Interactive mode for version selection'
67
+ method_option :list, type: :boolean, aliases: '-l', default: false,
68
+ desc: 'List all available Metanorma versions'
69
+ def install(version = nil)
70
+ cmd = InstallCommand.new
71
+ cmd.options = Thor::CoreExt::HashWithIndifferentAccess.new(options.merge(version: version))
72
+ cmd.install(version)
73
+ end
74
+
75
+ desc 'use [VERSION]', 'Set Metanorma version for current shell session'
76
+ method_option :source, type: :string, enum: %w[gemfile binary],
77
+ desc: 'Source type (gemfile or binary)'
78
+ method_option :interactive, type: :boolean, aliases: '-i', default: false,
79
+ desc: 'Interactive mode for version selection'
80
+ def use(version = nil)
81
+ cmd = VersionCommand.new
82
+ cmd.options = options
83
+ cmd.use(version)
84
+ end
85
+
86
+ desc 'global [VERSION]', 'Set default Metanorma version globally'
87
+ method_option :source, type: :string, enum: %w[gemfile binary],
88
+ desc: 'Source type (gemfile or binary)'
89
+ method_option :interactive, type: :boolean, aliases: '-i', default: false,
90
+ desc: 'Interactive mode for version selection'
91
+ def global(version = nil)
92
+ cmd = VersionCommand.new
93
+ cmd.options = options
94
+ cmd.global(version)
95
+ end
96
+
97
+ desc 'local [VERSION]', 'Set Metanorma version for current directory'
98
+ method_option :source, type: :string, enum: %w[gemfile binary],
99
+ desc: 'Source type (gemfile or binary)'
100
+ method_option :interactive, type: :boolean, aliases: '-i', default: false,
101
+ desc: 'Interactive mode for version selection'
102
+ def local(version = nil)
103
+ cmd = VersionCommand.new
104
+ cmd.options = options
105
+ cmd.local(version)
106
+ end
107
+
108
+ desc 'versions', 'List all installed Metanorma versions'
109
+ def versions
110
+ cmd = VersionCommand.new
111
+ cmd.versions
112
+ end
113
+
114
+ desc 'uninstall VERSION', 'Uninstall a Metanorma version'
115
+ method_option :force, type: :boolean, aliases: '-f', default: false,
116
+ desc: 'Force uninstallation without confirmation'
117
+ def uninstall(version)
118
+ cmd = UninstallCommand.new
119
+ cmd.options = options
120
+ cmd.uninstall(version)
121
+ end
122
+
30
123
  # General commands
31
124
  PLATFORM_REPOSITORIES = {
32
125
  'gemfile' => GemfileRepository,
33
126
  'snap' => SnapRepository,
34
127
  'homebrew' => HomebrewRepository,
35
- 'chocolatey' => ChocolateyRepository
128
+ 'chocolatey' => ChocolateyRepository,
129
+ 'binary' => BinaryRepository
36
130
  }.freeze
37
131
 
38
132
  desc 'info PLATFORM VERSION', 'Get details for a specific version'
@@ -54,8 +148,6 @@ module Mnenv
54
148
 
55
149
  case options[:format]
56
150
  when 'json'
57
- require_relative 'json_formatter'
58
- require 'json'
59
151
  output = JsonFormatter.format_version(version_obj)
60
152
  output['platform'] = platform
61
153
  puts JSON.pretty_generate(output)
@@ -66,6 +158,7 @@ module Mnenv
66
158
  puts " Parsed: #{version_obj.parsed_at || 'N/A'}"
67
159
  version_obj.to_h.each do |k, v|
68
160
  next if %w[version published_at parsed_at].include?(k)
161
+
69
162
  puts " #{k}: #{v}"
70
163
  end
71
164
  end
@@ -120,5 +213,12 @@ module Mnenv
120
213
  def version
121
214
  puts "mnenv #{Mnenv::VERSION}"
122
215
  end
216
+
217
+ desc 'update', 'Update version data from metanorma/versions repository'
218
+ def update
219
+ puts 'Updating version data...'
220
+ Repository.update_versions
221
+ puts 'Version data updated successfully.'
222
+ end
123
223
  end
124
224
  end
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require_relative '../version'
5
+ require_relative '../binary_repository'
6
+
7
+ module Mnenv
8
+ class AvailableCommand < Thor
9
+ namespace :available
10
+
11
+ class_option :source, type: :string, enum: %w[gemfile binary all], default: 'all',
12
+ desc: 'Source type to show (gemfile, binary, or all)'
13
+ class_option :format, type: :string, aliases: '-f', default: 'text',
14
+ desc: 'Output format (text or json)'
15
+
16
+ desc 'gemfile', 'List available Metanorma versions from RubyGems (gemfile method)'
17
+ method_option :format, type: :string, aliases: '-f', default: 'text'
18
+ def gemfile
19
+ repo = GemfileRepository.new
20
+ versions = repo.all.sort.reverse
21
+
22
+ show_available_versions('gemfile', versions)
23
+ end
24
+
25
+ desc 'binary', 'List available Metanorma versions from GitHub releases (binary method)'
26
+ method_option :format, type: :string, aliases: '-f', default: 'text'
27
+ def binary
28
+ repo = BinaryRepository.new
29
+ versions = repo.all
30
+
31
+ show_available_versions('binary', versions)
32
+ end
33
+
34
+ desc 'all', 'List all available Metanorma versions from all sources'
35
+ method_option :format, type: :string, aliases: '-f', default: 'text'
36
+ def all
37
+ case options[:format]
38
+ when 'json'
39
+ output = {}
40
+ output['gemfile'] = format_versions_json(GemfileRepository.new.all.sort.reverse)
41
+ output['binary'] = format_versions_json(BinaryRepository.new.all)
42
+ puts JSON.pretty_generate(output)
43
+ else
44
+ puts "\n=== Gemfile (RubyGems) ==="
45
+ gemfile
46
+
47
+ puts "\n=== Binary (packed-mn releases) ==="
48
+ binary
49
+ end
50
+ end
51
+
52
+ default_task :all
53
+
54
+ private
55
+
56
+ def show_available_versions(source, versions)
57
+ current_version, current_source = resolve_current
58
+
59
+ case options[:format]
60
+ when 'json'
61
+ output = {
62
+ 'source' => source,
63
+ 'versions' => format_versions_json(versions)
64
+ }
65
+ puts JSON.pretty_generate(output)
66
+ else
67
+ puts "Available Metanorma versions (source: #{source}):"
68
+
69
+ versions.each do |v|
70
+ # Check if installed
71
+ version_dir = File.expand_path("~/.mnenv/versions/#{v.version}")
72
+ installed = Dir.exist?(version_dir)
73
+
74
+ # Check installed source
75
+ installed_source = if installed
76
+ source_file = File.join(version_dir, 'source')
77
+ File.exist?(source_file) ? File.read(source_file).strip : 'unknown'
78
+ end
79
+
80
+ # Check if currently active
81
+ is_current = installed && v.version == current_version && installed_source == current_source
82
+
83
+ # Build status indicator
84
+ status_parts = []
85
+ status_parts << 'current' if is_current
86
+ status_parts << 'installed' if installed && !is_current
87
+ status = status_parts.empty? ? '' : " [#{status_parts.join(', ')}]"
88
+
89
+ # Show installed source if different
90
+ source_info = if installed && installed_source && installed_source != source
91
+ " (as #{installed_source})"
92
+ elsif installed && installed_source == source
93
+ ''
94
+ end
95
+
96
+ # Version display with padding
97
+ version_display = v.display_name
98
+ marker = is_current ? '*' : ' '
99
+ padding = ' ' * (15 - version_display.length - status.length)
100
+
101
+ puts " #{marker} #{version_display}#{padding}#{status}#{source_info}"
102
+ end
103
+ end
104
+ end
105
+
106
+ def format_versions_json(versions)
107
+ versions.map do |v|
108
+ version_dir = File.expand_path("~/.mnenv/versions/#{v.version}")
109
+ installed = Dir.exist?(version_dir)
110
+ installed_source = if installed
111
+ source_file = File.join(version_dir, 'source')
112
+ File.exist?(source_file) ? File.read(source_file).strip : nil
113
+ end
114
+
115
+ {
116
+ 'version' => v.version,
117
+ 'display_name' => v.display_name,
118
+ 'published_at' => v.published_at&.iso8601,
119
+ 'installed' => installed,
120
+ 'installed_source' => installed_source
121
+ }
122
+ end
123
+ end
124
+
125
+ def resolve_current
126
+ # Get current version
127
+ version = ENV['METANORMA_VERSION']
128
+ unless version
129
+ # Check .metanorma-version file
130
+ dir = Dir.pwd
131
+ while dir && dir != '/'
132
+ if File.exist?(File.join(dir, '.metanorma-version'))
133
+ version = File.read(File.join(dir, '.metanorma-version')).strip
134
+ break
135
+ end
136
+ dir = File.dirname(dir)
137
+ end
138
+
139
+ # Check global version
140
+ version ||= begin
141
+ global_version_file = File.expand_path('~/.mnenv/version')
142
+ File.read(global_version_file).strip if File.exist?(global_version_file)
143
+ end
144
+ end
145
+
146
+ # Get current source
147
+ source = ENV['METANORMA_SOURCE']
148
+ unless source
149
+ # Check .metanorma-source file
150
+ dir = Dir.pwd
151
+ while dir && dir != '/'
152
+ if File.exist?(File.join(dir, '.metanorma-source'))
153
+ source = File.read(File.join(dir, '.metanorma-source')).strip
154
+ break
155
+ end
156
+ dir = File.dirname(dir)
157
+ end
158
+
159
+ # Check global source
160
+ source ||= begin
161
+ global_source_file = File.expand_path('~/.mnenv/source')
162
+ File.read(global_source_file).strip if File.exist?(global_source_file)
163
+ end
164
+ end
165
+
166
+ [version, source]
167
+ end
168
+ end
169
+ end
@@ -4,12 +4,14 @@ require 'thor'
4
4
  require 'json'
5
5
 
6
6
  module Mnenv
7
+ autoload :ChocolateyRepository, 'mnenv/chocolatey_repository'
8
+ autoload :JsonFormatter, 'mnenv/json_formatter'
9
+ autoload :Chocolatey, 'mnenv/chocolatey'
10
+
7
11
  class ChocolateyCommand < Thor
8
12
  desc 'list', 'List all Chocolatey versions'
9
13
  method_option :format, type: :string, aliases: '-f', default: 'text'
10
14
  def list
11
- require_relative '../chocolatey_repository'
12
- require_relative '../json_formatter'
13
15
  repo = ChocolateyRepository.new
14
16
  versions = repo.all
15
17
 
@@ -25,7 +27,6 @@ module Mnenv
25
27
 
26
28
  desc 'refresh', 'Fetch and add new Chocolatey versions'
27
29
  def refresh
28
- require_relative '../chocolatey/fetcher'
29
30
  fetcher = Chocolatey::Fetcher.new
30
31
  existing = fetcher.repository.all.map(&:version)
31
32
  remote_versions = fetcher.fetch_all
@@ -41,7 +42,6 @@ module Mnenv
41
42
 
42
43
  desc 'revamp', 'Re-fetch all Chocolatey versions'
43
44
  def revamp
44
- require_relative '../chocolatey/fetcher'
45
45
  fetcher = Chocolatey::Fetcher.new
46
46
  versions = fetcher.fetch_and_save
47
47
  puts "Revamped #{versions.size} Chocolatey versions"
@@ -49,7 +49,6 @@ module Mnenv
49
49
 
50
50
  desc 'update VERSION', 'Update a specific Chocolatey version'
51
51
  def update(version)
52
- require_relative '../chocolatey/fetcher'
53
52
  fetcher = Chocolatey::Fetcher.new
54
53
  versions = fetcher.fetch_all
55
54
  target = versions.find { |v| v.version == version }
@@ -4,12 +4,14 @@ require 'thor'
4
4
  require 'json'
5
5
 
6
6
  module Mnenv
7
+ autoload :GemfileRepository, 'mnenv/gemfile_repository'
8
+ autoload :JsonFormatter, 'mnenv/json_formatter'
9
+ autoload :Gemfile, 'mnenv/gemfile'
10
+
7
11
  class GemfileCommand < Thor
8
12
  desc 'list', 'List all Gemfile (Ruby) versions'
9
13
  method_option :format, type: :string, aliases: '-f', default: 'text'
10
14
  def list
11
- require_relative '../gemfile_repository'
12
- require_relative '../json_formatter'
13
15
  repo = GemfileRepository.new
14
16
  versions = repo.all
15
17
 
@@ -25,21 +27,18 @@ module Mnenv
25
27
 
26
28
  desc 'refresh', 'Extract new Gemfiles (incremental mode)'
27
29
  def refresh
28
- require_relative '../gemfile/extractor'
29
30
  extractor = Gemfile::Extractor.new(mode: :incremental)
30
31
  extractor.run
31
32
  end
32
33
 
33
34
  desc 'revamp', 'Re-extract all Gemfiles'
34
35
  def revamp
35
- require_relative '../gemfile/extractor'
36
36
  extractor = Gemfile::Extractor.new(mode: :revamp)
37
37
  extractor.run
38
38
  end
39
39
 
40
40
  desc 'update VERSION', 'Re-extract specific version'
41
41
  def update(version)
42
- require_relative '../gemfile/extractor'
43
42
  extractor = Gemfile::Extractor.new(mode: :replace, target_version: version)
44
43
  extractor.run
45
44
  end
@@ -4,12 +4,14 @@ require 'thor'
4
4
  require 'json'
5
5
 
6
6
  module Mnenv
7
+ autoload :HomebrewRepository, 'mnenv/homebrew_repository'
8
+ autoload :JsonFormatter, 'mnenv/json_formatter'
9
+ autoload :Homebrew, 'mnenv/homebrew'
10
+
7
11
  class HomebrewCommand < Thor
8
12
  desc 'list', 'List all Homebrew versions'
9
13
  method_option :format, type: :string, aliases: '-f', default: 'text'
10
14
  def list
11
- require_relative '../homebrew_repository'
12
- require_relative '../json_formatter'
13
15
  repo = HomebrewRepository.new
14
16
  versions = repo.all
15
17
 
@@ -25,7 +27,6 @@ module Mnenv
25
27
 
26
28
  desc 'refresh', 'Fetch and add new Homebrew versions'
27
29
  def refresh
28
- require_relative '../homebrew/fetcher'
29
30
  fetcher = Homebrew::Fetcher.new
30
31
  existing = fetcher.repository.all.map(&:version)
31
32
  remote_versions = fetcher.fetch_all
@@ -41,7 +42,6 @@ module Mnenv
41
42
 
42
43
  desc 'revamp', 'Re-fetch all Homebrew versions'
43
44
  def revamp
44
- require_relative '../homebrew/fetcher'
45
45
  fetcher = Homebrew::Fetcher.new
46
46
  versions = fetcher.fetch_and_save
47
47
  puts "Revamped #{versions.size} Homebrew versions"
@@ -49,7 +49,6 @@ module Mnenv
49
49
 
50
50
  desc 'update VERSION', 'Update a specific Homebrew version'
51
51
  def update(version)
52
- require_relative '../homebrew/fetcher'
53
52
  fetcher = Homebrew::Fetcher.new
54
53
  versions = fetcher.fetch_all
55
54
  target = versions.find { |v| v.version == version }
@@ -0,0 +1,234 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tty/prompt'
4
+ require_relative '../installer'
5
+ require_relative '../binary_repository'
6
+
7
+ module Mnenv
8
+ class InstallCommand < Thor
9
+ namespace :install
10
+
11
+ class_option :source, type: :string, enum: %w[gemfile binary], default: 'gemfile',
12
+ desc: 'Installation source (gemfile or binary)'
13
+ class_option :interactive, type: :boolean, aliases: '-i', default: false,
14
+ desc: 'Interactive mode for version selection'
15
+
16
+ desc 'VERSION', 'Install a specific Metanorma version'
17
+ method_option :source, type: :string, enum: %w[gemfile binary], default: 'gemfile',
18
+ desc: 'Installation source (gemfile or binary)'
19
+ method_option :interactive, type: :boolean, aliases: '-i', default: false,
20
+ desc: 'Interactive mode for version selection'
21
+ method_option :list, type: :boolean, aliases: '-l', default: false,
22
+ desc: 'List all available Metanorma versions'
23
+ def install(version = nil)
24
+ # If --list flag is provided, show available versions and exit
25
+ return list_available_versions if options[:list]
26
+
27
+ # If no version provided or interactive mode, prompt for selection
28
+ if options[:interactive] || version.nil?
29
+ version, source = select_installation_interactive
30
+ else
31
+ source = options[:source]
32
+ end
33
+
34
+ installer = InstallerFactory.create(version, source: source)
35
+
36
+ if installer.installed?
37
+ prompt = TTY::Prompt.new
38
+ unless prompt.yes?("Version #{version} (source: #{source}) is already installed. Reinstall?")
39
+ puts 'Installation cancelled.'
40
+ return
41
+ end
42
+ end
43
+
44
+ puts "Installing Metanorma #{version} (source: #{source})..."
45
+ installer.install
46
+ puts "Successfully installed Metanorma #{version} (source: #{source})!"
47
+ rescue Installer::InstallationError => e
48
+ warn "Error: #{e.message}"
49
+ exit 1
50
+ end
51
+
52
+ default_task :install
53
+
54
+ private
55
+
56
+ def list_available_versions
57
+ current_version, current_source = resolve_current
58
+ current_platform = detect_platform
59
+
60
+ puts "\nAvailable Metanorma versions:"
61
+ puts "(gemfile = source build, binary = prebuilt for specific platforms)\n"
62
+
63
+ # Get all unique versions from both sources
64
+ gemfile_repo = GemfileRepository.new
65
+ binary_repo = BinaryRepository.new
66
+
67
+ gemfile_versions = gemfile_repo.all.sort.reverse
68
+ binary_versions = binary_repo.all
69
+
70
+ # Combine all versions
71
+ all_versions = Hash.new { |h, k| h[k] = { gemfile: false, binary: false } }
72
+
73
+ gemfile_versions.each do |v|
74
+ all_versions[v.version][:gemfile] = true
75
+ all_versions[v.version][:gemfile_obj] = v
76
+ end
77
+
78
+ binary_versions.each do |v|
79
+ all_versions[v.version][:binary] = true
80
+ all_versions[v.version][:binary_obj] = v
81
+ all_versions[v.version][:binary_platforms] = extract_platforms(v.assets)
82
+ end
83
+
84
+ # Check what's installed (from installed directory, not versions data)
85
+ if Dir.exist?(Paths::INSTALLED_DIR)
86
+ Dir.glob("#{Paths::INSTALLED_DIR}/*/").each do |dir|
87
+ version = File.basename(dir)
88
+ source_file = File.join(dir, 'source')
89
+ source = File.exist?(source_file) ? File.read(source_file).strip : nil
90
+ all_versions[version][:"installed_#{source}"] = true if source && all_versions[version]
91
+ end
92
+ end
93
+
94
+ # Display sorted versions (newest first)
95
+ all_versions.sort.reverse.each do |version, info|
96
+ # Build sources string with platform info for binary
97
+ sources = []
98
+ sources << 'gemfile' if info[:gemfile]
99
+ if info[:binary]
100
+ platforms = info[:binary_platforms] || []
101
+ if platforms.any?
102
+ # Highlight current platform
103
+ platform_display = platforms.map do |p|
104
+ p == current_platform ? "#{p}*" : p
105
+ end.join(', ')
106
+ sources << "binary [#{platform_display}]"
107
+ else
108
+ sources << 'binary'
109
+ end
110
+ end
111
+
112
+ # Check if current
113
+ is_current = version == current_version && sources.any? { |s| s.include?(current_source.to_s) }
114
+ marker = is_current ? '* ' : ' '
115
+
116
+ # Version display
117
+ version_display = version.ljust(15)
118
+
119
+ # Sources display
120
+ sources_display = sources.join(', ')
121
+
122
+ # Installed status
123
+ installed = []
124
+ installed << 'gemfile' if info[:installed_gemfile]
125
+ installed << 'binary' if info[:installed_binary]
126
+ installed_display = installed.empty? ? '' : " [installed: #{installed.join(', ')}]"
127
+
128
+ puts " #{marker}#{version_display}(#{sources_display})#{installed_display}"
129
+ end
130
+
131
+ # Legend
132
+ puts "\nLegend:"
133
+ puts ' * Current version / platform'
134
+ puts ' [installed: ...] = Already installed locally'
135
+ puts " Detected platform: #{current_platform}"
136
+ puts "\nInstall with: mnenv install VERSION --source SOURCE"
137
+ puts 'Examples:'
138
+ puts ' mnenv install 1.14.4 --source gemfile'
139
+ puts ' mnenv install 1.14.4 --source binary'
140
+ end
141
+
142
+ def extract_platforms(assets)
143
+ return [] unless assets
144
+
145
+ assets.filter_map do |asset|
146
+ # Match patterns like metanorma-darwin-arm64.tgz or metanorma-linux-x86_64.tgz
147
+ case asset
148
+ when /darwin/ then 'macos'
149
+ when /linux/ then 'linux'
150
+ when /windows/ then 'windows'
151
+ end
152
+ end.uniq.sort
153
+ end
154
+
155
+ def detect_platform
156
+ case RbConfig::CONFIG['host_os']
157
+ when /linux/ then 'linux'
158
+ when /darwin/ then 'macos'
159
+ when /mswin|mingw|cygwin/ then 'windows'
160
+ else 'unknown'
161
+ end
162
+ end
163
+
164
+ def resolve_current
165
+ # Get current version
166
+ version = ENV['METANORMA_VERSION']
167
+ unless version
168
+ # Check .metanorma-version file
169
+ dir = Dir.pwd
170
+ while dir && dir != '/'
171
+ if File.exist?(File.join(dir, '.metanorma-version'))
172
+ version = File.read(File.join(dir, '.metanorma-version')).strip
173
+ break
174
+ end
175
+ dir = File.dirname(dir)
176
+ end
177
+
178
+ # Check global version
179
+ version ||= (File.read(Paths::VERSION_FILE).strip if File.exist?(Paths::VERSION_FILE))
180
+
181
+ end
182
+
183
+ # Get current source
184
+ source = ENV['METANORMA_SOURCE']
185
+ unless source
186
+ # Check .metanorma-source file
187
+ dir = Dir.pwd
188
+ while dir && dir != '/'
189
+ if File.exist?(File.join(dir, '.metanorma-source'))
190
+ source = File.read(File.join(dir, '.metanorma-source')).strip
191
+ break
192
+ end
193
+ dir = File.dirname(dir)
194
+ end
195
+
196
+ # Check global source
197
+ source ||= (File.read(Paths::SOURCE_FILE).strip if File.exist?(Paths::SOURCE_FILE))
198
+
199
+ end
200
+
201
+ [version, source]
202
+ end
203
+
204
+ def select_installation_interactive
205
+ repo = GemfileRepository.new
206
+ prompt = TTY::Prompt.new
207
+
208
+ # First, select source
209
+ source = prompt.select('Select installation source:', [
210
+ { name: 'Gemfile (faster, on-demand loading, requires dev tools, upgradable)',
211
+ value: 'gemfile' },
212
+ { name: 'Binary (slower, full-stack memory load, no dev tools, fixed)',
213
+ value: 'binary' }
214
+ ])
215
+
216
+ # Then, select version
217
+ choices = repo.all.sort.map do |v|
218
+ version_dir = Paths.version_install_dir(v.version)
219
+ installed = Dir.exist?(version_dir) && File.exist?(File.join(version_dir, 'source'))
220
+ installed_source = if installed
221
+ src = File.read(File.join(version_dir, 'source')).strip
222
+ src.empty? ? '' : "(#{src})"
223
+ else
224
+ ''
225
+ end
226
+ { name: "#{v.display_name} #{installed ? "[installed: #{installed_source}]" : ''}", value: v.version }
227
+ end
228
+
229
+ version = prompt.select('Select a version to install:', choices)
230
+
231
+ [version, source]
232
+ end
233
+ end
234
+ end