octo-agent 0.11.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 (319) hide show
  1. checksums.yaml +7 -0
  2. data/.clacky/skills/commit/SKILL.md +423 -0
  3. data/.clacky/skills/gem-release/SKILL.md +199 -0
  4. data/.clacky/skills/gem-release/scripts/release.sh +304 -0
  5. data/.clacky/skills/oss-upload/SKILL.md +47 -0
  6. data/.octorules +106 -0
  7. data/.rspec +3 -0
  8. data/.rubocop.yml +8 -0
  9. data/CHANGELOG.md +76 -0
  10. data/CODE_OF_CONDUCT.md +132 -0
  11. data/CONTRIBUTING.md +92 -0
  12. data/Dockerfile +28 -0
  13. data/LICENSE.txt +22 -0
  14. data/POSITIONING.md +46 -0
  15. data/README.md +134 -0
  16. data/README_CN.md +134 -0
  17. data/Rakefile +34 -0
  18. data/benchmark/fixtures/sample_project/Gemfile +3 -0
  19. data/benchmark/fixtures/sample_project/lib/api_handler.rb +32 -0
  20. data/benchmark/fixtures/sample_project/lib/order_calculator.rb +23 -0
  21. data/benchmark/fixtures/sample_project/lib/user_renderer.rb +20 -0
  22. data/benchmark/fixtures/sample_project/spec/order_calculator_spec.rb +20 -0
  23. data/benchmark/results/EVALUATION_REPORT.md +165 -0
  24. data/benchmark/results/baseline_20260511_174424.json +128 -0
  25. data/benchmark/results/report_20260511_175256.json +271 -0
  26. data/benchmark/results/report_20260511_175444.json +271 -0
  27. data/benchmark/results/treatment_20260511_175103.json +130 -0
  28. data/benchmark/runner.rb +441 -0
  29. data/bin/octo +7 -0
  30. data/docs/agent-first-ui-design.md +77 -0
  31. data/docs/billing-system.md +318 -0
  32. data/docs/channel-architecture.md +235 -0
  33. data/docs/engineering-article.md +343 -0
  34. data/docs/session-skill-invocation.md +69 -0
  35. data/docs/time_machine_design.md +247 -0
  36. data/docs/ui2-architecture.md +124 -0
  37. data/homebrew/README.md +96 -0
  38. data/homebrew/openocto.rb +24 -0
  39. data/lib/octo/agent/hook_manager.rb +61 -0
  40. data/lib/octo/agent/llm_caller.rb +800 -0
  41. data/lib/octo/agent/memory_updater.rb +246 -0
  42. data/lib/octo/agent/message_compressor.rb +225 -0
  43. data/lib/octo/agent/message_compressor_helper.rb +869 -0
  44. data/lib/octo/agent/next_message_suggester.rb +215 -0
  45. data/lib/octo/agent/session_serializer.rb +685 -0
  46. data/lib/octo/agent/skill_auto_creator.rb +114 -0
  47. data/lib/octo/agent/skill_evolution.rb +61 -0
  48. data/lib/octo/agent/skill_manager.rb +466 -0
  49. data/lib/octo/agent/skill_reflector.rb +89 -0
  50. data/lib/octo/agent/system_prompt_builder.rb +101 -0
  51. data/lib/octo/agent/time_machine.rb +214 -0
  52. data/lib/octo/agent/tool_executor.rb +454 -0
  53. data/lib/octo/agent/tool_registry.rb +150 -0
  54. data/lib/octo/agent.rb +2180 -0
  55. data/lib/octo/agent_config.rb +989 -0
  56. data/lib/octo/agent_profile.rb +112 -0
  57. data/lib/octo/anthropic_stream_aggregator.rb +137 -0
  58. data/lib/octo/background_task_registry.rb +324 -0
  59. data/lib/octo/banner.rb +34 -0
  60. data/lib/octo/bedrock_stream_aggregator.rb +137 -0
  61. data/lib/octo/block_font.rb +331 -0
  62. data/lib/octo/cli.rb +968 -0
  63. data/lib/octo/client.rb +623 -0
  64. data/lib/octo/default_agents/SOUL.md +3 -0
  65. data/lib/octo/default_agents/USER.md +1 -0
  66. data/lib/octo/default_agents/base_prompt.md +66 -0
  67. data/lib/octo/default_agents/coding/profile.yml +2 -0
  68. data/lib/octo/default_agents/coding/system_prompt.md +67 -0
  69. data/lib/octo/default_agents/general/profile.yml +2 -0
  70. data/lib/octo/default_agents/general/system_prompt.md +16 -0
  71. data/lib/octo/default_parsers/doc_parser.rb +69 -0
  72. data/lib/octo/default_parsers/docx_parser.rb +188 -0
  73. data/lib/octo/default_parsers/pdf_parser.rb +120 -0
  74. data/lib/octo/default_parsers/pdf_parser_ocr.py +103 -0
  75. data/lib/octo/default_parsers/pdf_parser_plumber.py +62 -0
  76. data/lib/octo/default_parsers/pptx_parser.rb +140 -0
  77. data/lib/octo/default_parsers/xlsx_parser.rb +121 -0
  78. data/lib/octo/default_skills/browser-setup/SKILL.md +426 -0
  79. data/lib/octo/default_skills/channel-manager/SKILL.md +623 -0
  80. data/lib/octo/default_skills/channel-manager/dingtalk_setup.rb +191 -0
  81. data/lib/octo/default_skills/channel-manager/discord_setup.rb +199 -0
  82. data/lib/octo/default_skills/channel-manager/feishu_setup.rb +574 -0
  83. data/lib/octo/default_skills/channel-manager/import_lark_skills.rb +97 -0
  84. data/lib/octo/default_skills/channel-manager/install_feishu_skills.rb +105 -0
  85. data/lib/octo/default_skills/channel-manager/weixin_setup.rb +274 -0
  86. data/lib/octo/default_skills/code-explorer/SKILL.md +36 -0
  87. data/lib/octo/default_skills/cron-task-creator/SKILL.md +257 -0
  88. data/lib/octo/default_skills/cron-task-creator/evals/evals.json +38 -0
  89. data/lib/octo/default_skills/onboard/SKILL.md +578 -0
  90. data/lib/octo/default_skills/onboard/scripts/import_external_skills.rb +413 -0
  91. data/lib/octo/default_skills/onboard/scripts/install_builtin_skills.rb +97 -0
  92. data/lib/octo/default_skills/persist-memory/SKILL.md +59 -0
  93. data/lib/octo/default_skills/personal-website/SKILL.md +113 -0
  94. data/lib/octo/default_skills/personal-website/publish.rb +235 -0
  95. data/lib/octo/default_skills/product-help/SKILL.md +123 -0
  96. data/lib/octo/default_skills/product-help/docs/agent-config.md +74 -0
  97. data/lib/octo/default_skills/product-help/docs/best-practices.md +49 -0
  98. data/lib/octo/default_skills/product-help/docs/browser-tool.md +53 -0
  99. data/lib/octo/default_skills/product-help/docs/built-in-skills.md +43 -0
  100. data/lib/octo/default_skills/product-help/docs/cli-reference.md +82 -0
  101. data/lib/octo/default_skills/product-help/docs/create-your-first-skill.md +47 -0
  102. data/lib/octo/default_skills/product-help/docs/faq.md +98 -0
  103. data/lib/octo/default_skills/product-help/docs/how-to-use-a-skill.md +58 -0
  104. data/lib/octo/default_skills/product-help/docs/installation.md +59 -0
  105. data/lib/octo/default_skills/product-help/docs/memory-system.md +61 -0
  106. data/lib/octo/default_skills/product-help/docs/octorules.md +62 -0
  107. data/lib/octo/default_skills/product-help/docs/session-management.md +63 -0
  108. data/lib/octo/default_skills/product-help/docs/skill-basics.md +55 -0
  109. data/lib/octo/default_skills/product-help/docs/skill-frontmatter.md +61 -0
  110. data/lib/octo/default_skills/product-help/docs/web-server.md +49 -0
  111. data/lib/octo/default_skills/product-help/docs/what-is-octo.md +37 -0
  112. data/lib/octo/default_skills/product-help/docs/windows-installation.md +36 -0
  113. data/lib/octo/default_skills/product-help/docs/writing-tips.md +53 -0
  114. data/lib/octo/default_skills/recall-memory/SKILL.md +65 -0
  115. data/lib/octo/default_skills/skill-add/SKILL.md +59 -0
  116. data/lib/octo/default_skills/skill-add/scripts/install_from_zip.rb +295 -0
  117. data/lib/octo/default_skills/skill-creator/SKILL.md +602 -0
  118. data/lib/octo/default_skills/skill-creator/agents/analyzer.md +274 -0
  119. data/lib/octo/default_skills/skill-creator/agents/comparator.md +202 -0
  120. data/lib/octo/default_skills/skill-creator/agents/grader.md +223 -0
  121. data/lib/octo/default_skills/skill-creator/eval-viewer/generate_review.py +471 -0
  122. data/lib/octo/default_skills/skill-creator/eval-viewer/viewer.html +1325 -0
  123. data/lib/octo/default_skills/skill-creator/references/schemas.md +430 -0
  124. data/lib/octo/default_skills/skill-creator/scripts/__init__.py +0 -0
  125. data/lib/octo/default_skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
  126. data/lib/octo/default_skills/skill-creator/scripts/generate_report.py +326 -0
  127. data/lib/octo/default_skills/skill-creator/scripts/improve_description.py +310 -0
  128. data/lib/octo/default_skills/skill-creator/scripts/quick_validate.py +103 -0
  129. data/lib/octo/default_skills/skill-creator/scripts/run_eval.py +317 -0
  130. data/lib/octo/default_skills/skill-creator/scripts/run_loop.py +331 -0
  131. data/lib/octo/default_skills/skill-creator/scripts/utils.py +47 -0
  132. data/lib/octo/default_skills/skill-creator/scripts/validate_skill_frontmatter.rb +143 -0
  133. data/lib/octo/idle_compression_timer.rb +115 -0
  134. data/lib/octo/json_ui_controller.rb +204 -0
  135. data/lib/octo/message_format/anthropic.rb +409 -0
  136. data/lib/octo/message_format/bedrock.rb +361 -0
  137. data/lib/octo/message_format/open_ai.rb +222 -0
  138. data/lib/octo/message_history.rb +373 -0
  139. data/lib/octo/openai_stream_aggregator.rb +130 -0
  140. data/lib/octo/plain_ui_controller.rb +166 -0
  141. data/lib/octo/providers.rb +534 -0
  142. data/lib/octo/server/browser_manager.rb +397 -0
  143. data/lib/octo/server/channel/adapters/base.rb +82 -0
  144. data/lib/octo/server/channel/adapters/dingtalk/adapter.rb +314 -0
  145. data/lib/octo/server/channel/adapters/dingtalk/api_client.rb +391 -0
  146. data/lib/octo/server/channel/adapters/dingtalk/stream_client.rb +203 -0
  147. data/lib/octo/server/channel/adapters/discord/adapter.rb +229 -0
  148. data/lib/octo/server/channel/adapters/discord/api_client.rb +107 -0
  149. data/lib/octo/server/channel/adapters/discord/gateway_client.rb +270 -0
  150. data/lib/octo/server/channel/adapters/feishu/adapter.rb +320 -0
  151. data/lib/octo/server/channel/adapters/feishu/bot.rb +478 -0
  152. data/lib/octo/server/channel/adapters/feishu/file_processor.rb +36 -0
  153. data/lib/octo/server/channel/adapters/feishu/message_parser.rb +129 -0
  154. data/lib/octo/server/channel/adapters/feishu/ws_client.rb +423 -0
  155. data/lib/octo/server/channel/adapters/telegram/adapter.rb +375 -0
  156. data/lib/octo/server/channel/adapters/telegram/api_client.rb +205 -0
  157. data/lib/octo/server/channel/adapters/wecom/adapter.rb +148 -0
  158. data/lib/octo/server/channel/adapters/wecom/media_downloader.rb +115 -0
  159. data/lib/octo/server/channel/adapters/wecom/ws_client.rb +395 -0
  160. data/lib/octo/server/channel/adapters/weixin/adapter.rb +692 -0
  161. data/lib/octo/server/channel/adapters/weixin/api_client.rb +402 -0
  162. data/lib/octo/server/channel/channel_config.rb +178 -0
  163. data/lib/octo/server/channel/channel_manager.rb +468 -0
  164. data/lib/octo/server/channel/channel_ui_controller.rb +224 -0
  165. data/lib/octo/server/channel.rb +33 -0
  166. data/lib/octo/server/discover.rb +77 -0
  167. data/lib/octo/server/epipe_safe_io.rb +105 -0
  168. data/lib/octo/server/http_server.rb +3554 -0
  169. data/lib/octo/server/scheduler.rb +317 -0
  170. data/lib/octo/server/server_master.rb +325 -0
  171. data/lib/octo/server/session_registry.rb +431 -0
  172. data/lib/octo/server/web_ui_controller.rb +487 -0
  173. data/lib/octo/session_manager.rb +385 -0
  174. data/lib/octo/skill.rb +466 -0
  175. data/lib/octo/skill_loader.rb +328 -0
  176. data/lib/octo/tools/base.rb +118 -0
  177. data/lib/octo/tools/browser.rb +625 -0
  178. data/lib/octo/tools/edit.rb +165 -0
  179. data/lib/octo/tools/file_reader.rb +549 -0
  180. data/lib/octo/tools/glob.rb +162 -0
  181. data/lib/octo/tools/grep.rb +356 -0
  182. data/lib/octo/tools/invoke_skill.rb +96 -0
  183. data/lib/octo/tools/list_tasks.rb +54 -0
  184. data/lib/octo/tools/redo_task.rb +41 -0
  185. data/lib/octo/tools/request_user_feedback.rb +84 -0
  186. data/lib/octo/tools/security.rb +333 -0
  187. data/lib/octo/tools/terminal/output_cleaner.rb +63 -0
  188. data/lib/octo/tools/terminal/persistent_session.rb +268 -0
  189. data/lib/octo/tools/terminal/safe_rm.sh +106 -0
  190. data/lib/octo/tools/terminal/session_manager.rb +213 -0
  191. data/lib/octo/tools/terminal.rb +1828 -0
  192. data/lib/octo/tools/todo_manager.rb +374 -0
  193. data/lib/octo/tools/trash_manager.rb +388 -0
  194. data/lib/octo/tools/undo_task.rb +35 -0
  195. data/lib/octo/tools/web_fetch.rb +242 -0
  196. data/lib/octo/tools/web_search.rb +260 -0
  197. data/lib/octo/tools/write.rb +77 -0
  198. data/lib/octo/ui2/block_font.rb +10 -0
  199. data/lib/octo/ui2/components/base_component.rb +163 -0
  200. data/lib/octo/ui2/components/command_suggestions.rb +290 -0
  201. data/lib/octo/ui2/components/common_component.rb +96 -0
  202. data/lib/octo/ui2/components/inline_input.rb +226 -0
  203. data/lib/octo/ui2/components/input_area.rb +1338 -0
  204. data/lib/octo/ui2/components/message_component.rb +99 -0
  205. data/lib/octo/ui2/components/modal_component.rb +419 -0
  206. data/lib/octo/ui2/components/todo_area.rb +149 -0
  207. data/lib/octo/ui2/components/tool_component.rb +107 -0
  208. data/lib/octo/ui2/components/welcome_banner.rb +139 -0
  209. data/lib/octo/ui2/layout_manager.rb +807 -0
  210. data/lib/octo/ui2/line_editor.rb +363 -0
  211. data/lib/octo/ui2/markdown_renderer.rb +100 -0
  212. data/lib/octo/ui2/output_buffer.rb +370 -0
  213. data/lib/octo/ui2/progress_handle.rb +362 -0
  214. data/lib/octo/ui2/progress_indicator.rb +55 -0
  215. data/lib/octo/ui2/screen_buffer.rb +273 -0
  216. data/lib/octo/ui2/terminal_detector.rb +119 -0
  217. data/lib/octo/ui2/theme_manager.rb +85 -0
  218. data/lib/octo/ui2/themes/base_theme.rb +105 -0
  219. data/lib/octo/ui2/themes/hacker_theme.rb +62 -0
  220. data/lib/octo/ui2/themes/minimal_theme.rb +56 -0
  221. data/lib/octo/ui2/thinking_verbs.rb +26 -0
  222. data/lib/octo/ui2/ui_controller.rb +1625 -0
  223. data/lib/octo/ui2/view_renderer.rb +177 -0
  224. data/lib/octo/ui2.rb +40 -0
  225. data/lib/octo/ui_interface.rb +154 -0
  226. data/lib/octo/utils/arguments_parser.rb +191 -0
  227. data/lib/octo/utils/browser_detector.rb +195 -0
  228. data/lib/octo/utils/encoding.rb +92 -0
  229. data/lib/octo/utils/environment_detector.rb +140 -0
  230. data/lib/octo/utils/file_ignore_helper.rb +170 -0
  231. data/lib/octo/utils/file_processor.rb +601 -0
  232. data/lib/octo/utils/gitignore_parser.rb +154 -0
  233. data/lib/octo/utils/limit_stack.rb +152 -0
  234. data/lib/octo/utils/logger.rb +124 -0
  235. data/lib/octo/utils/login_shell.rb +72 -0
  236. data/lib/octo/utils/model_pricing.rb +646 -0
  237. data/lib/octo/utils/parser_manager.rb +165 -0
  238. data/lib/octo/utils/path_helper.rb +15 -0
  239. data/lib/octo/utils/scripts_manager.rb +59 -0
  240. data/lib/octo/utils/string_matcher.rb +158 -0
  241. data/lib/octo/utils/trash_directory.rb +112 -0
  242. data/lib/octo/utils/workspace_rules.rb +46 -0
  243. data/lib/octo/version.rb +5 -0
  244. data/lib/octo/web/app.css +7141 -0
  245. data/lib/octo/web/app.js +543 -0
  246. data/lib/octo/web/apple-touch-icon.png +0 -0
  247. data/lib/octo/web/auth.js +150 -0
  248. data/lib/octo/web/channels.js +276 -0
  249. data/lib/octo/web/datepicker.js +205 -0
  250. data/lib/octo/web/favicon.png +0 -0
  251. data/lib/octo/web/i18n.js +1073 -0
  252. data/lib/octo/web/icon-512.png +0 -0
  253. data/lib/octo/web/icon-dark.svg +25 -0
  254. data/lib/octo/web/icon.svg +29 -0
  255. data/lib/octo/web/index.html +871 -0
  256. data/lib/octo/web/marked.min.js +69 -0
  257. data/lib/octo/web/onboard.js +491 -0
  258. data/lib/octo/web/profile.js +442 -0
  259. data/lib/octo/web/sessions.js +4421 -0
  260. data/lib/octo/web/settings.js +913 -0
  261. data/lib/octo/web/sidebar.js +32 -0
  262. data/lib/octo/web/skills.js +885 -0
  263. data/lib/octo/web/tasks.js +297 -0
  264. data/lib/octo/web/theme.js +105 -0
  265. data/lib/octo/web/trash.js +343 -0
  266. data/lib/octo/web/vendor/hljs/highlight.min.js +1244 -0
  267. data/lib/octo/web/vendor/hljs/hljs-theme.css +95 -0
  268. data/lib/octo/web/vendor/katex/auto-render.min.js +1 -0
  269. data/lib/octo/web/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  270. data/lib/octo/web/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  271. data/lib/octo/web/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  272. data/lib/octo/web/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  273. data/lib/octo/web/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  274. data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  275. data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  276. data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  277. data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  278. data/lib/octo/web/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  279. data/lib/octo/web/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  280. data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  281. data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  282. data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  283. data/lib/octo/web/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  284. data/lib/octo/web/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  285. data/lib/octo/web/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  286. data/lib/octo/web/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  287. data/lib/octo/web/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  288. data/lib/octo/web/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  289. data/lib/octo/web/vendor/katex/katex.min.css +1 -0
  290. data/lib/octo/web/vendor/katex/katex.min.js +1 -0
  291. data/lib/octo/web/version.js +449 -0
  292. data/lib/octo/web/weixin-qr.html +209 -0
  293. data/lib/octo/web/ws-dispatcher.js +357 -0
  294. data/lib/octo/web/ws.js +128 -0
  295. data/lib/octo.rb +145 -0
  296. data/scripts/build/build.sh +329 -0
  297. data/scripts/build/lib/apt.sh +56 -0
  298. data/scripts/build/lib/brew.sh +89 -0
  299. data/scripts/build/lib/colors.sh +17 -0
  300. data/scripts/build/lib/gem.sh +95 -0
  301. data/scripts/build/lib/mise.sh +125 -0
  302. data/scripts/build/lib/network.sh +157 -0
  303. data/scripts/build/lib/os.sh +57 -0
  304. data/scripts/build/lib/shell.sh +37 -0
  305. data/scripts/build/src/install.sh.cc +174 -0
  306. data/scripts/build/src/install_browser.sh.cc +101 -0
  307. data/scripts/build/src/install_full.sh.cc +290 -0
  308. data/scripts/build/src/install_rails_deps.sh.cc +145 -0
  309. data/scripts/build/src/install_system_deps.sh.cc +123 -0
  310. data/scripts/build/src/uninstall.sh.cc +101 -0
  311. data/scripts/install.ps1 +532 -0
  312. data/scripts/install.sh +567 -0
  313. data/scripts/install_browser.sh +479 -0
  314. data/scripts/install_full.sh +838 -0
  315. data/scripts/install_rails_deps.sh +746 -0
  316. data/scripts/install_system_deps.sh +518 -0
  317. data/scripts/uninstall.sh +287 -0
  318. data/sig/octo.rbs +4 -0
  319. metadata +614 -0
@@ -0,0 +1,746 @@
1
+ #!/bin/bash
2
+ # install_rails_deps.sh — install Ruby 3.3+ and Node.js 22+ via mise, and PostgreSQL for Rails development
3
+ # Generated from scripts/build/src/install_rails_deps.sh.cc — DO NOT EDIT DIRECTLY
4
+ #
5
+ # Usage:
6
+ # bash install_rails_deps.sh # install ruby + node + postgres
7
+ # bash install_rails_deps.sh ruby # ruby only
8
+ # bash install_rails_deps.sh node # node only
9
+ # bash install_rails_deps.sh postgres # postgres only
10
+
11
+ set -e
12
+
13
+ INSTALL_TARGET="${1:-all}" # all | ruby | node | postgres
14
+
15
+
16
+ # ---[ @include lib/colors.sh ]---
17
+
18
+ RED='\033[0;31m'
19
+ GREEN='\033[0;32m'
20
+ YELLOW='\033[1;33m'
21
+ BLUE='\033[0;34m'
22
+ NC='\033[0m'
23
+
24
+ print_info() { echo -e "${BLUE}ℹ${NC} $1"; }
25
+ print_success() { echo -e "${GREEN}✓${NC} $1"; }
26
+ print_warning() { echo -e "${YELLOW}⚠${NC} $1"; }
27
+ print_error() { echo -e "${RED}✗${NC} $1"; }
28
+ print_step() { echo -e "\n${BLUE}==>${NC} $1"; }
29
+
30
+
31
+ # ---[ @include lib/os.sh ]---
32
+
33
+ # Sets OS (macOS | Linux | Windows | Unknown) and DISTRO (ubuntu | debian | …)
34
+ detect_os() {
35
+ case "$(uname -s)" in
36
+ Linux*) OS=Linux ;;
37
+ Darwin*) OS=macOS ;;
38
+ CYGWIN*) OS=Windows ;;
39
+ MINGW*) OS=Windows ;;
40
+ *) OS=Unknown ;;
41
+ esac
42
+ print_info "Detected OS: $OS"
43
+
44
+ if [ "$OS" = "Linux" ] && [ -f /etc/os-release ]; then
45
+ # shellcheck source=/dev/null
46
+ . /etc/os-release
47
+ DISTRO=$ID
48
+ print_info "Detected Linux distribution: $DISTRO"
49
+ else
50
+ DISTRO=unknown
51
+ fi
52
+ }
53
+
54
+ # Returns 0 if the given command is on PATH
55
+ command_exists() { command -v "$1" >/dev/null 2>&1; }
56
+
57
+ # Boolean helpers — use these in business logic instead of inline string comparisons
58
+ is_macos() { [ "$OS" = "macOS" ]; }
59
+ is_linux_apt() { [ "$OS" = "Linux" ] && { [ "$DISTRO" = "ubuntu" ] || [ "$DISTRO" = "debian" ]; }; }
60
+
61
+ # Returns 0 (true) if $1 >= $2 (semantic version comparison)
62
+ version_ge() { printf '%s\n%s\n' "$2" "$1" | sort -V -C; }
63
+
64
+ # Assert that the current OS/distro is supported (macOS or Ubuntu/Debian).
65
+ # Optional $1: hint message printed on failure (e.g. manual install instructions).
66
+ # Exits with code 1 on unsupported OS or distro.
67
+ assert_supported_os() {
68
+ local hint="${1:-}"
69
+ if [ "$OS" = "Linux" ]; then
70
+ if [ "$DISTRO" = "ubuntu" ] || [ "$DISTRO" = "debian" ]; then
71
+ return 0
72
+ fi
73
+ print_error "Unsupported Linux distribution: $DISTRO"
74
+ [ -n "$hint" ] && print_info "$hint"
75
+ exit 1
76
+ elif [ "$OS" = "macOS" ]; then
77
+ return 0
78
+ else
79
+ print_error "Unsupported OS: $OS"
80
+ [ -n "$hint" ] && print_info "$hint"
81
+ exit 1
82
+ fi
83
+ }
84
+
85
+
86
+ # ---[ @include lib/shell.sh ]---
87
+
88
+ # Sets CURRENT_SHELL (zsh | bash | fish) and SHELL_RC (path to rc file)
89
+ detect_shell() {
90
+ local shell_name
91
+ shell_name=$(basename "$SHELL")
92
+
93
+ case "$shell_name" in
94
+ zsh)
95
+ CURRENT_SHELL="zsh"
96
+ SHELL_RC="$HOME/.zshrc"
97
+ ;;
98
+ fish)
99
+ CURRENT_SHELL="fish"
100
+ SHELL_RC="$HOME/.config/fish/config.fish"
101
+ ;;
102
+ bash)
103
+ CURRENT_SHELL="bash"
104
+ # macOS uses ~/.bash_profile; Linux uses ~/.bashrc
105
+ if is_macos; then
106
+ SHELL_RC="$HOME/.bash_profile"
107
+ else
108
+ SHELL_RC="$HOME/.bashrc"
109
+ fi
110
+ ;;
111
+ *)
112
+ CURRENT_SHELL="bash"
113
+ SHELL_RC="$HOME/.bashrc"
114
+ ;;
115
+ esac
116
+
117
+ print_info "Detected shell: $CURRENT_SHELL (rc file: $SHELL_RC)"
118
+ }
119
+
120
+
121
+ # ---[ @include lib/network.sh ]---
122
+
123
+ # --------------------------------------------------------------------------
124
+ # Mirror variables — overridden by detect_network_region()
125
+ # --------------------------------------------------------------------------
126
+ SLOW_THRESHOLD_MS=5000
127
+ NETWORK_REGION="global" # china | global | unknown
128
+ USE_CN_MIRRORS=false
129
+
130
+ GITHUB_RAW_BASE_URL="https://raw.githubusercontent.com"
131
+ DEFAULT_RUBYGEMS_URL="https://rubygems.org"
132
+ DEFAULT_NPM_REGISTRY="https://registry.npmjs.org"
133
+ DEFAULT_MISE_INSTALL_URL="https://mise.run"
134
+
135
+ CN_CDN_BASE_URL="https://oss.1024code.com"
136
+ CN_ALIYUN_MIRROR="https://mirrors.aliyun.com"
137
+ CN_MISE_INSTALL_URL="${CN_CDN_BASE_URL}/mise.sh"
138
+ CN_RUBY_PRECOMPILED_URL="${CN_CDN_BASE_URL}/ruby/ruby-{version}.{platform}.tar.gz"
139
+ CN_RUBYGEMS_URL="${CN_ALIYUN_MIRROR}/rubygems/"
140
+ CN_NPM_REGISTRY="https://registry.npmmirror.com"
141
+ CN_NODE_MIRROR_URL="https://cdn.npmmirror.com/binaries/node/"
142
+ CN_GEM_BASE_URL="${CN_CDN_BASE_URL}/octo"
143
+ CN_GEM_LATEST_URL="${CN_GEM_BASE_URL}/latest.txt"
144
+
145
+ # Active values (set by detect_network_region)
146
+ MISE_INSTALL_URL="$DEFAULT_MISE_INSTALL_URL"
147
+ RUBYGEMS_INSTALL_URL="$DEFAULT_RUBYGEMS_URL"
148
+ NPM_REGISTRY_URL="$DEFAULT_NPM_REGISTRY"
149
+ NODE_MIRROR_URL="" # empty = mise default (nodejs.org)
150
+ RUBY_VERSION_SPEC="ruby@3" # CN mode pins to a specific precompiled build
151
+
152
+ # --------------------------------------------------------------------------
153
+ # Internal probe helpers
154
+ # --------------------------------------------------------------------------
155
+
156
+ # Probe a single URL; echoes round-trip time in ms, or "timeout"
157
+ _probe_url() {
158
+ local url="$1"
159
+ local out http_code total_time
160
+ out=$(curl -s -o /dev/null -w "%{http_code} %{time_total}" \
161
+ --connect-timeout 5 --max-time 5 "$url" 2>/dev/null) || true
162
+ http_code="${out%% *}"
163
+ total_time="${out#* }"
164
+ if [ -z "$http_code" ] || [ "$http_code" = "000" ] || [ "$http_code" = "$out" ]; then
165
+ echo "timeout"; return
166
+ fi
167
+ awk -v s="$total_time" 'BEGIN { printf "%d", s * 1000 }'
168
+ }
169
+
170
+ # Returns 0 (true) if result is slow or unreachable
171
+ _is_slow_or_unreachable() {
172
+ local r="$1"
173
+ [ "$r" = "timeout" ] && return 0
174
+ [ "${r:-9999}" -ge "$SLOW_THRESHOLD_MS" ] 2>/dev/null
175
+ }
176
+
177
+ _format_probe_time() {
178
+ local r="$1"
179
+ [ "$r" = "timeout" ] && echo "timeout" && return
180
+ awk -v ms="$r" 'BEGIN { printf "%.1fs", ms / 1000 }'
181
+ }
182
+
183
+ _print_probe_result() {
184
+ local label="$1" result="$2"
185
+ if [ "$result" = "timeout" ]; then
186
+ print_warning "UNREACHABLE ${label}"
187
+ elif _is_slow_or_unreachable "$result"; then
188
+ print_warning "SLOW ($(_format_probe_time "$result")) ${label}"
189
+ else
190
+ print_success "OK ($(_format_probe_time "$result")) ${label}"
191
+ fi
192
+ }
193
+
194
+ # Probe URL up to max_retries times; returns first fast result or last result
195
+ _probe_url_with_retry() {
196
+ local url="$1" max="${2:-2}" result
197
+ for _ in $(seq 1 "$max"); do
198
+ result=$(_probe_url "$url")
199
+ ! _is_slow_or_unreachable "$result" && { echo "$result"; return 0; }
200
+ done
201
+ echo "$result"
202
+ }
203
+
204
+ # --------------------------------------------------------------------------
205
+ # detect_network_region — sets USE_CN_MIRRORS and active mirror variables
206
+ # --------------------------------------------------------------------------
207
+ detect_network_region() {
208
+ print_step "Network pre-flight check..."
209
+ echo ""
210
+
211
+ local google_result baidu_result
212
+ google_result=$(_probe_url "https://www.google.com")
213
+ baidu_result=$(_probe_url "https://www.baidu.com")
214
+
215
+ _print_probe_result "google.com" "$google_result"
216
+ _print_probe_result "baidu.com" "$baidu_result"
217
+
218
+ local google_ok=false baidu_ok=false
219
+ ! _is_slow_or_unreachable "$google_result" && google_ok=true
220
+ ! _is_slow_or_unreachable "$baidu_result" && baidu_ok=true
221
+
222
+ if [ "$google_ok" = true ]; then
223
+ NETWORK_REGION="global"
224
+ print_success "Region: global"
225
+ elif [ "$baidu_ok" = true ]; then
226
+ NETWORK_REGION="china"
227
+ print_success "Region: china"
228
+ else
229
+ NETWORK_REGION="unknown"
230
+ print_warning "Region: unknown (both unreachable)"
231
+ fi
232
+ echo ""
233
+
234
+ if [ "$NETWORK_REGION" = "china" ]; then
235
+ local cdn_result mirror_result
236
+ cdn_result=$(_probe_url_with_retry "$CN_MISE_INSTALL_URL")
237
+ mirror_result=$(_probe_url_with_retry "$CN_RUBYGEMS_URL")
238
+
239
+ _print_probe_result "CN CDN (mise/Ruby)" "$cdn_result"
240
+ _print_probe_result "Aliyun (gem)" "$mirror_result"
241
+
242
+ local cdn_ok=false mirror_ok=false
243
+ ! _is_slow_or_unreachable "$cdn_result" && cdn_ok=true
244
+ ! _is_slow_or_unreachable "$mirror_result" && mirror_ok=true
245
+
246
+ if [ "$cdn_ok" = true ] || [ "$mirror_ok" = true ]; then
247
+ USE_CN_MIRRORS=true
248
+ MISE_INSTALL_URL="$CN_MISE_INSTALL_URL"
249
+ RUBYGEMS_INSTALL_URL="$CN_RUBYGEMS_URL"
250
+ NPM_REGISTRY_URL="$CN_NPM_REGISTRY"
251
+ NODE_MIRROR_URL="$CN_NODE_MIRROR_URL"
252
+ RUBY_VERSION_SPEC="ruby@3.4.8"
253
+ print_info "CN mirrors applied"
254
+ else
255
+ print_warning "CN mirrors unreachable — falling back to global sources"
256
+ fi
257
+ else
258
+ local rubygems_result mise_result
259
+ rubygems_result=$(_probe_url_with_retry "$DEFAULT_RUBYGEMS_URL")
260
+ mise_result=$(_probe_url_with_retry "$DEFAULT_MISE_INSTALL_URL")
261
+
262
+ _print_probe_result "RubyGems" "$rubygems_result"
263
+ _print_probe_result "mise.run" "$mise_result"
264
+
265
+ _is_slow_or_unreachable "$rubygems_result" && print_warning "RubyGems is slow/unreachable."
266
+ _is_slow_or_unreachable "$mise_result" && print_warning "mise.run is slow/unreachable."
267
+
268
+ USE_CN_MIRRORS=false
269
+ RUBY_VERSION_SPEC="ruby@3"
270
+ fi
271
+
272
+ echo ""
273
+ }
274
+
275
+
276
+ # ---[ @include lib/apt.sh ]---
277
+
278
+ # Configure apt mirror for CN region and run apt-get update.
279
+ # Guards: only runs on ubuntu/debian ($DISTRO).
280
+ # Relies on $USE_CN_MIRRORS set by detect_network_region (network.sh).
281
+ setup_apt_mirror() {
282
+ [ "$DISTRO" = "ubuntu" ] || [ "$DISTRO" = "debian" ] || return 0
283
+
284
+ if [ "$USE_CN_MIRRORS" = true ]; then
285
+ print_info "Region: China — configuring Aliyun apt mirror"
286
+
287
+ if [ -f /etc/apt/sources.list ]; then
288
+ sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak
289
+ print_info "Backed up /etc/apt/sources.list to sources.list.bak"
290
+ fi
291
+
292
+ if [ "$DISTRO" = "debian" ]; then
293
+ local codename="${VERSION_CODENAME:-bookworm}"
294
+ local components="main contrib non-free non-free-firmware"
295
+ local mirror="${CN_ALIYUN_MIRROR}/debian/"
296
+ local security_mirror="${CN_ALIYUN_MIRROR}/debian-security/"
297
+ sudo tee /etc/apt/sources.list > /dev/null <<EOF
298
+ deb ${mirror} ${codename} ${components}
299
+ deb ${mirror} ${codename}-updates ${components}
300
+ deb ${mirror} ${codename}-backports ${components}
301
+ deb ${security_mirror} ${codename}-security ${components}
302
+ EOF
303
+ else
304
+ local codename="${VERSION_CODENAME:-jammy}"
305
+ local components="main restricted universe multiverse"
306
+ local arch; arch=$(dpkg --print-architecture 2>/dev/null || uname -m)
307
+ if [ "$arch" = "arm64" ] || [ "$arch" = "aarch64" ]; then
308
+ local mirror="${CN_ALIYUN_MIRROR}/ubuntu-ports/"
309
+ else
310
+ local mirror="${CN_ALIYUN_MIRROR}/ubuntu/"
311
+ fi
312
+ sudo tee /etc/apt/sources.list > /dev/null <<EOF
313
+ deb ${mirror} ${codename} ${components}
314
+ deb ${mirror} ${codename}-updates ${components}
315
+ deb ${mirror} ${codename}-backports ${components}
316
+ deb ${mirror} ${codename}-security ${components}
317
+ EOF
318
+ fi
319
+
320
+ print_success "apt mirror set to Aliyun"
321
+ else
322
+ print_info "Region: global — using default apt sources"
323
+ fi
324
+
325
+ sudo apt-get update -qq
326
+ print_success "apt updated"
327
+ }
328
+
329
+
330
+ # ---[ @include lib/brew.sh ]---
331
+
332
+ # --------------------------------------------------------------------------
333
+ # Homebrew CN mirror URLs (Aliyun)
334
+ # --------------------------------------------------------------------------
335
+ CN_HOMEBREW_INSTALL_SCRIPT_URL="${CN_CDN_BASE_URL}/Homebrew/install/HEAD/install.sh"
336
+ CN_HOMEBREW_BREW_GIT_REMOTE="https://mirrors.aliyun.com/homebrew/brew.git"
337
+ CN_HOMEBREW_CORE_GIT_REMOTE="https://mirrors.aliyun.com/homebrew/homebrew-core.git"
338
+ CN_HOMEBREW_BOTTLE_DOMAIN="https://mirrors.aliyun.com/homebrew/homebrew-bottles"
339
+ CN_HOMEBREW_API_DOMAIN="https://mirrors.aliyun.com/homebrew-bottles/api"
340
+
341
+ HOMEBREW_INSTALL_SCRIPT_URL="https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh"
342
+
343
+ # --------------------------------------------------------------------------
344
+ # configure_homebrew_cn_mirrors — export env vars and persist to SHELL_RC
345
+ # Only runs when USE_CN_MIRRORS=true
346
+ # --------------------------------------------------------------------------
347
+ configure_homebrew_cn_mirrors() {
348
+ [ "$USE_CN_MIRRORS" = true ] || return 0
349
+
350
+ print_info "Configuring Homebrew CN mirrors..."
351
+ export HOMEBREW_INSTALL_FROM_API=1
352
+ export HOMEBREW_API_DOMAIN="$CN_HOMEBREW_API_DOMAIN"
353
+ export HOMEBREW_BREW_GIT_REMOTE="$CN_HOMEBREW_BREW_GIT_REMOTE"
354
+ export HOMEBREW_CORE_GIT_REMOTE="$CN_HOMEBREW_CORE_GIT_REMOTE"
355
+ export HOMEBREW_BOTTLE_DOMAIN="$CN_HOMEBREW_BOTTLE_DOMAIN"
356
+
357
+ if ! grep -q "HOMEBREW_BOTTLE_DOMAIN" "$SHELL_RC" 2>/dev/null; then
358
+ {
359
+ echo ""
360
+ echo "# Homebrew CN mirrors (added by octo installer)"
361
+ echo "export HOMEBREW_INSTALL_FROM_API=1"
362
+ echo "export HOMEBREW_API_DOMAIN=\"${CN_HOMEBREW_API_DOMAIN}\""
363
+ echo "export HOMEBREW_BREW_GIT_REMOTE=\"${CN_HOMEBREW_BREW_GIT_REMOTE}\""
364
+ echo "export HOMEBREW_CORE_GIT_REMOTE=\"${CN_HOMEBREW_CORE_GIT_REMOTE}\""
365
+ echo "export HOMEBREW_BOTTLE_DOMAIN=\"${CN_HOMEBREW_BOTTLE_DOMAIN}\""
366
+ } >> "$SHELL_RC"
367
+ print_success "Homebrew CN mirrors written to $SHELL_RC"
368
+ else
369
+ print_success "Homebrew CN mirrors already configured in $SHELL_RC"
370
+ fi
371
+ }
372
+
373
+ # --------------------------------------------------------------------------
374
+ # restore_homebrew_cn_mirrors — remove CN mirror lines from SHELL_RC
375
+ # --------------------------------------------------------------------------
376
+ restore_homebrew_cn_mirrors() {
377
+ if [ -n "$SHELL_RC" ] && [ -f "$SHELL_RC" ] && grep -q "HOMEBREW_BOTTLE_DOMAIN" "$SHELL_RC" 2>/dev/null; then
378
+ sed -i.bak '/# Homebrew CN mirrors (added by octo installer)/d' "$SHELL_RC"
379
+ sed -i.bak '/HOMEBREW_INSTALL_FROM_API/d' "$SHELL_RC"
380
+ sed -i.bak '/HOMEBREW_API_DOMAIN/d' "$SHELL_RC"
381
+ sed -i.bak '/HOMEBREW_BREW_GIT_REMOTE/d' "$SHELL_RC"
382
+ sed -i.bak '/HOMEBREW_CORE_GIT_REMOTE/d' "$SHELL_RC"
383
+ sed -i.bak '/HOMEBREW_BOTTLE_DOMAIN/d' "$SHELL_RC"
384
+ rm -f "${SHELL_RC}.bak"
385
+ unset HOMEBREW_BREW_GIT_REMOTE HOMEBREW_CORE_GIT_REMOTE HOMEBREW_BOTTLE_DOMAIN
386
+ print_success "Homebrew CN mirrors removed from $SHELL_RC"
387
+ else
388
+ print_info "Homebrew CN mirrors — nothing to restore"
389
+ fi
390
+ }
391
+
392
+ # --------------------------------------------------------------------------
393
+ # ensure_homebrew — install Homebrew if missing, add to PATH
394
+ # --------------------------------------------------------------------------
395
+ ensure_homebrew() {
396
+ if command_exists brew; then
397
+ print_success "Homebrew already installed"
398
+ return 0
399
+ fi
400
+
401
+ print_info "Installing Homebrew..."
402
+ local brew_url="$HOMEBREW_INSTALL_SCRIPT_URL"
403
+ [ "$USE_CN_MIRRORS" = true ] && brew_url="$CN_HOMEBREW_INSTALL_SCRIPT_URL"
404
+
405
+ if /bin/bash -c "$(curl -fsSL "$brew_url")"; then
406
+ # Add Homebrew to PATH (Apple Silicon default path)
407
+ echo 'export PATH="/opt/homebrew/bin:$PATH"' >> "$SHELL_RC"
408
+ export PATH="/opt/homebrew/bin:$PATH"
409
+ print_success "Homebrew installed"
410
+ else
411
+ print_error "Failed to install Homebrew"
412
+ return 1
413
+ fi
414
+ }
415
+
416
+
417
+ # ---[ @include lib/mise.sh ]---
418
+
419
+ # --------------------------------------------------------------------------
420
+ # Internal helper — locate the mise binary
421
+ # --------------------------------------------------------------------------
422
+ _mise_bin() {
423
+ if [ -x "$HOME/.local/bin/mise" ]; then
424
+ echo "$HOME/.local/bin/mise"
425
+ elif command_exists mise; then
426
+ command -v mise
427
+ else
428
+ echo ""
429
+ fi
430
+ }
431
+
432
+ # --------------------------------------------------------------------------
433
+ # ensure_mise — install mise if missing, activate it in the current session
434
+ # --------------------------------------------------------------------------
435
+ ensure_mise() {
436
+ local mise
437
+ mise=$(_mise_bin)
438
+
439
+ if [ -n "$mise" ]; then
440
+ print_success "mise already installed: $($mise --version 2>/dev/null || echo 'n/a')"
441
+ export PATH="$HOME/.local/bin:$PATH"
442
+ eval "$($mise activate bash 2>/dev/null)" 2>/dev/null || true
443
+ MISE_BIN="$mise"
444
+ return 0
445
+ fi
446
+
447
+ print_step "Installing mise..."
448
+ if curl -fsSL "$MISE_INSTALL_URL" | sh; then
449
+ export PATH="$HOME/.local/bin:$PATH"
450
+ eval "$(~/.local/bin/mise activate bash 2>/dev/null)" 2>/dev/null || true
451
+
452
+ # Persist mise activation to shell rc
453
+ local init_line='eval "$(~/.local/bin/mise activate '"$CURRENT_SHELL"')"'
454
+ if ! grep -q "mise activate" "$SHELL_RC" 2>/dev/null; then
455
+ echo "$init_line" >> "$SHELL_RC"
456
+ print_info "Added mise activation to $SHELL_RC"
457
+ fi
458
+
459
+ MISE_BIN="$HOME/.local/bin/mise"
460
+ print_success "mise installed"
461
+ else
462
+ print_error "Failed to install mise"
463
+ return 1
464
+ fi
465
+ }
466
+
467
+ # --------------------------------------------------------------------------
468
+ # install_ruby_via_mise — install Ruby via mise
469
+ # CN mode: precompiled binary from oss.1024code.com
470
+ # Global: mise default (precompiled where available)
471
+ # --------------------------------------------------------------------------
472
+ install_ruby_via_mise() {
473
+ local mise="${MISE_BIN:-$(_mise_bin)}"
474
+ if [ -z "$mise" ]; then
475
+ print_error "mise not found — call ensure_mise first"
476
+ return 1
477
+ fi
478
+
479
+ print_info "Installing Ruby via mise ($RUBY_VERSION_SPEC)..."
480
+
481
+ if [ "$USE_CN_MIRRORS" = true ]; then
482
+ "$mise" settings ruby.compile=false 2>/dev/null || true
483
+ "$mise" settings ruby.precompiled_url="$CN_RUBY_PRECOMPILED_URL" 2>/dev/null || true
484
+ print_info "Using precompiled Ruby from CN CDN"
485
+ else
486
+ # Enable precompiled binaries globally (mise supports this on common platforms)
487
+ "$mise" settings ruby.compile=false 2>/dev/null || true
488
+ "$mise" settings unset ruby.precompiled_url 2>/dev/null || true
489
+ fi
490
+
491
+ if "$mise" use -g "$RUBY_VERSION_SPEC"; then
492
+ eval "$($mise activate bash 2>/dev/null)" 2>/dev/null || true
493
+ local installed
494
+ installed=$(ruby -e 'puts RUBY_VERSION' 2>/dev/null || echo "unknown")
495
+ print_success "Ruby $installed installed"
496
+ else
497
+ print_error "Failed to install Ruby via mise"
498
+ return 1
499
+ fi
500
+ }
501
+
502
+ # --------------------------------------------------------------------------
503
+ # install_node_via_mise — install Node.js 22 via mise
504
+ # --------------------------------------------------------------------------
505
+ install_node_via_mise() {
506
+ local mise="${MISE_BIN:-$(_mise_bin)}"
507
+ if [ -z "$mise" ]; then
508
+ print_error "mise not found — call ensure_mise first"
509
+ return 1
510
+ fi
511
+
512
+ # Skip if compatible Node is already active
513
+ if command_exists node; then
514
+ local ver major
515
+ ver=$(node --version 2>/dev/null | sed 's/v//')
516
+ major="${ver%%.*}"
517
+ if [ "${major:-0}" -ge 22 ] 2>/dev/null; then
518
+ print_success "Node.js v${ver} already satisfies >= 22 — skipping"
519
+ return 0
520
+ fi
521
+ fi
522
+
523
+ # Apply CN Node mirror if needed
524
+ if [ "$USE_CN_MIRRORS" = true ] && [ -n "$NODE_MIRROR_URL" ]; then
525
+ "$mise" settings node.mirror_url="$NODE_MIRROR_URL" 2>/dev/null || true
526
+ print_info "Node mirror → ${NODE_MIRROR_URL}"
527
+ fi
528
+
529
+ print_info "Installing Node.js 22 via mise..."
530
+ if "$mise" use -g node@22; then
531
+ eval "$($mise activate bash 2>/dev/null)" 2>/dev/null || true
532
+ print_success "Node.js $(node --version 2>/dev/null) installed"
533
+ else
534
+ print_error "Failed to install Node.js via mise"
535
+ return 1
536
+ fi
537
+ }
538
+
539
+
540
+ # ---[ @include lib/gem.sh ]---
541
+
542
+ configure_gem_source() {
543
+ local gemrc="$HOME/.gemrc"
544
+
545
+ if [ "$USE_CN_MIRRORS" = true ]; then
546
+ if grep -q "${CN_RUBYGEMS_URL}" "$gemrc" 2>/dev/null; then
547
+ print_success "gem source already → ${CN_RUBYGEMS_URL}"
548
+ else
549
+ [ -f "$gemrc" ] && mv "$gemrc" "$HOME/.gemrc_octobak"
550
+ cat > "$gemrc" <<GEMRC
551
+ :sources:
552
+ - ${CN_RUBYGEMS_URL}
553
+ GEMRC
554
+ print_success "gem source → ${CN_RUBYGEMS_URL}"
555
+ fi
556
+ else
557
+ if [ -f "$gemrc" ] && grep -q "${CN_RUBYGEMS_URL}" "$gemrc" 2>/dev/null; then
558
+ if [ -f "$HOME/.gemrc_octobak" ]; then
559
+ mv "$HOME/.gemrc_octobak" "$gemrc"
560
+ print_info "gem source restored from backup"
561
+ else
562
+ rm "$gemrc"
563
+ print_info "gem source restored to default"
564
+ fi
565
+ fi
566
+ fi
567
+ }
568
+
569
+ restore_gemrc() {
570
+ local gemrc="$HOME/.gemrc"
571
+ local gemrc_bak="$HOME/.gemrc_octobak"
572
+ if [ -f "$gemrc_bak" ]; then
573
+ mv "$gemrc_bak" "$gemrc"
574
+ print_success "~/.gemrc restored from backup"
575
+ elif [ -f "$gemrc" ]; then
576
+ rm "$gemrc"
577
+ print_success "~/.gemrc removed"
578
+ else
579
+ print_info "~/.gemrc — nothing to restore"
580
+ fi
581
+ }
582
+
583
+ # --------------------------------------------------------------------------
584
+ # Redirect GEM_HOME to user dir when system Ruby gem dir is not writable
585
+ # --------------------------------------------------------------------------
586
+ setup_gem_home() {
587
+ local gem_dir
588
+ gem_dir=$(gem environment gemdir 2>/dev/null || true)
589
+ [ -w "$gem_dir" ] && return 0
590
+
591
+ local ruby_api
592
+ ruby_api=$(ruby -e 'puts RbConfig::CONFIG["ruby_version"]' 2>/dev/null)
593
+ export GEM_HOME="$HOME/.gem/ruby/${ruby_api}"
594
+ export GEM_PATH="$HOME/.gem/ruby/${ruby_api}"
595
+ export PATH="$HOME/.gem/ruby/${ruby_api}/bin:$PATH"
596
+ print_info "System Ruby detected — gems will install to ~/.gem/ruby/${ruby_api}"
597
+
598
+ if [ -n "$SHELL_RC" ] && ! grep -q "GEM_HOME" "$SHELL_RC" 2>/dev/null; then
599
+ {
600
+ echo ""
601
+ echo "# Ruby user gem dir (added by octo installer)"
602
+ echo "export GEM_HOME=\"\$HOME/.gem/ruby/${ruby_api}\""
603
+ echo "export GEM_PATH=\"\$HOME/.gem/ruby/${ruby_api}\""
604
+ echo "export PATH=\"\$HOME/.gem/ruby/${ruby_api}/bin:\$PATH\""
605
+ } >> "$SHELL_RC"
606
+ print_info "GEM_HOME written to $SHELL_RC"
607
+ fi
608
+ }
609
+
610
+ restore_gem_home() {
611
+ [ -n "$SHELL_RC" ] && [ -f "$SHELL_RC" ] || return 0
612
+ grep -q "GEM_HOME" "$SHELL_RC" 2>/dev/null || return 0
613
+ # Remove the block written by setup_gem_home (comment + 3 export lines)
614
+ local tmp
615
+ tmp=$(mktemp)
616
+ grep -v "# Ruby user gem dir (added by octo installer)" "$SHELL_RC" \
617
+ | grep -v "export GEM_HOME=" \
618
+ | grep -v "export GEM_PATH=" \
619
+ | grep -v "/.gem/ruby/" \
620
+ > "$tmp" && mv "$tmp" "$SHELL_RC"
621
+ print_success "GEM_HOME removed from $SHELL_RC"
622
+ }
623
+
624
+
625
+ # --------------------------------------------------------------------------
626
+ # Ruby: install via mise and configure gem source
627
+ # --------------------------------------------------------------------------
628
+ install_ruby() {
629
+ print_step "Installing Ruby via mise..."
630
+
631
+ # Install Ruby compile-time dependencies first
632
+ if is_macos; then
633
+ brew install openssl@3 libyaml gmp
634
+ elif is_linux_apt; then
635
+ sudo apt-get install -y \
636
+ rustc libssl-dev libyaml-dev zlib1g-dev libgmp-dev
637
+ fi
638
+
639
+ ensure_mise || return 1
640
+ install_ruby_via_mise || return 1
641
+
642
+ # Configure gem source for CN users
643
+ configure_gem_source
644
+
645
+ # Reinstall octo in the new Ruby environment
646
+ "${MISE_BIN:-mise}" exec -- gem install octo --no-document \
647
+ && print_success "octo reinstalled" \
648
+ || print_warning "Could not reinstall octo — run manually: gem install octo --no-document"
649
+ }
650
+
651
+ # --------------------------------------------------------------------------
652
+ # Node: install via mise and configure npm registry
653
+ # --------------------------------------------------------------------------
654
+ install_node() {
655
+ print_step "Installing Node.js via mise..."
656
+
657
+ ensure_mise || return 1
658
+ install_node_via_mise || return 1
659
+
660
+ if [ "$USE_CN_MIRRORS" = true ] && command_exists npm; then
661
+ npm config set registry "$NPM_REGISTRY_URL" 2>/dev/null || true
662
+ print_info "npm registry → ${NPM_REGISTRY_URL}"
663
+ fi
664
+ }
665
+
666
+ # --------------------------------------------------------------------------
667
+ # PostgreSQL: install server + client via brew (macOS) or apt (Linux/WSL)
668
+ # --------------------------------------------------------------------------
669
+ install_postgres() {
670
+ print_step "Installing PostgreSQL..."
671
+
672
+ if is_macos; then
673
+ ensure_homebrew || return 1
674
+ brew install postgresql
675
+ brew services start postgresql
676
+
677
+ elif is_linux_apt; then
678
+ setup_apt_mirror
679
+ sudo apt-get install -y postgresql libpq-dev \
680
+ libssl-dev libreadline-dev zlib1g-dev libyaml-dev libffi-dev
681
+ sudo systemctl enable --now postgresql || true
682
+ fi
683
+
684
+ print_success "PostgreSQL installed"
685
+ }
686
+
687
+ # --------------------------------------------------------------------------
688
+ # Main
689
+ # --------------------------------------------------------------------------
690
+ main() {
691
+ echo ""
692
+ echo "╔═══════════════════════════════════════════════════════════╗"
693
+ echo "║ ║"
694
+ echo "║ 🔧 Rails Dependencies Installer ║"
695
+ echo "║ ║"
696
+ echo "╚═══════════════════════════════════════════════════════════╝"
697
+ echo ""
698
+
699
+ detect_shell
700
+ detect_network_region
701
+
702
+ # On macOS, Homebrew is required — if missing, guide the user to install_full.sh
703
+ if is_macos && ! command_exists brew; then
704
+ local install_cmd
705
+ if [ "$USE_CN_MIRRORS" = true ]; then
706
+ install_cmd='/bin/bash -c "$(curl -sSL https://oss.1024code.com/scripts/install_full.sh)"'
707
+ else
708
+ install_cmd='/bin/bash -c "$(curl -sSL https://raw.githubusercontent.com/octo-ai/octo/main/scripts/install_full.sh)"'
709
+ fi
710
+ echo ""
711
+ print_error "Homebrew is not installed — it is required to continue."
712
+ echo ""
713
+ echo " Homebrew installation requires your sudo password and interactive confirmation"
714
+ echo " — it cannot be run automatically by the AI agent."
715
+ echo ""
716
+ echo " Please open a new terminal window and run the full installer:"
717
+ echo ""
718
+ echo " ${install_cmd}"
719
+ echo ""
720
+ echo " This will install Homebrew, Ruby, Node.js, and all required dependencies."
721
+ echo " Once done, come back and try again."
722
+ echo ""
723
+ exit 1
724
+ fi
725
+
726
+ # Run system deps script if available
727
+ local sys_deps="$HOME/.octo/scripts/install_system_deps.sh"
728
+ [ -f "$sys_deps" ] && { bash "$sys_deps" || print_warning "System deps install had warnings — continuing"; }
729
+
730
+ case "$INSTALL_TARGET" in
731
+ ruby) install_ruby || exit 1 ;;
732
+ node) install_node || exit 1 ;;
733
+ postgres) install_postgres || exit 1 ;;
734
+ *)
735
+ install_ruby || exit 1
736
+ install_node || exit 1
737
+ install_postgres || exit 1
738
+ ;;
739
+ esac
740
+
741
+ echo ""
742
+ print_success "Done. Please re-source your shell or open a new terminal if paths changed."
743
+ echo ""
744
+ }
745
+
746
+ main "$@"