Module: VivlioStarter::CLI::DoctorCommands::ToolUpgrader
- Defined in:
- lib/vivlio_starter/cli/doctor/tool_upgrader.rb
Overview
vs upgrade の外部ツール更新フェーズの実装。 更新対象の列挙・現在/最新バージョンの取得・計画提示・実行・結果集計を担う。
Defined Under Namespace
Classes: Deps, Plan, PlanEntry, Tool, UpdateResult
Constant Summary collapse
- TEXTLINT_NPM_PACKAGES =
textlint と推奨ルール一式(npm -g)。--fix のインストールと --upgrade の更新で共用する
%w[ textlint textlint-rule-preset-ja-technical-writing textlint-rule-preset-japanese textlint-rule-prh textlint-filter-rule-node-types textlint-filter-rule-allowlist textlint-filter-rule-comments textlint-rule-no-dropping-the-ra textlint-rule-max-ten textlint-rule-ja-no-mixed-period textlint-rule-no-doubled-conjunction@3.0.0 textlint-rule-no-doubled-joshi textlint-rule-ja-no-successive-word textlint-rule-preset-ja-spacing textlint-rule-spellcheck-tech-word textlint-rule-no-dead-link textlint-rule-ng-word ].freeze
- TOOLS =
[ Tool.new(label: 'node', kind: :brew, package: 'node', checks: %w[node]), Tool.new(label: 'vivliostyle CLI', kind: :npm, package: '@vivliostyle/cli', checks: %w[vivliostyle]), Tool.new(label: 'textlint 一式', kind: :npm, package: :textlint_set, checks: %w[textlint]), Tool.new(label: 'mathjax-full', kind: :npm, package: 'mathjax-full', checks: %w[mathjax]), Tool.new(label: 'mermaid-cli', kind: :npm, package: '@mermaid-js/mermaid-cli', checks: %w[mermaid]), Tool.new(label: 'qpdf', kind: :brew, package: 'qpdf', checks: %w[qpdf]), Tool.new(label: 'poppler', kind: :brew, package: 'poppler', checks: %w[pdfinfo pdftoppm]), Tool.new(label: 'ghostscript', kind: :brew, package: 'ghostscript', checks: %w[gs]), Tool.new(label: 'imagemagick', kind: :brew, package: 'imagemagick', checks: %w[imagemagick]), Tool.new(label: 'librsvg', kind: :brew, package: 'librsvg', checks: %w[rsvg-convert]), Tool.new(label: 'vips', kind: :brew, package: 'vips', checks: %w[vips]), Tool.new(label: 'tesseract', kind: :brew, package: 'tesseract', checks: %w[tesseract]), Tool.new(label: 'tesseract-lang', kind: :brew, package: 'tesseract-lang', checks: %w[tesseract-lang]), Tool.new(label: 'mecab', kind: :brew, package: 'mecab', checks: %w[mecab]), Tool.new(label: 'mecab-ipadic', kind: :brew, package: 'mecab-ipadic', checks: %w[mecab]), Tool.new(label: 'inkscape', kind: :cask, package: 'inkscape', checks: %w[inkscape]), Tool.new(label: 'kindle-previewer', kind: :cask, package: 'kindle-previewer', checks: %w[kindlepreviewer]), # Enhanced Mode プラグイン(gem 名は provider.rb / doctor.rb と同値。導入済みの場合のみ更新対象) Tool.new(label: 'vivlio-starter-pdf', kind: :gem, package: 'vivlio-starter-pdf', checks: []), Tool.new(label: 'waifu2x', kind: :manual, package: nil, checks: []), Tool.new(label: 'rouge', kind: :manual, package: nil, checks: []) ].freeze
- MANUAL_NOTES =
更新対象外(:manual)の理由(計画表の注記)
{ 'waifu2x' => '導入経路が多様', 'rouge' => '本 gem の依存として Bundler 管理' }.freeze
- QUERIES =
一括問い合わせコマンド。ツールごとの個別問い合わせはしない(遅いため・spec §2.2)。 npm outdated は「差分あり」でも exit 1 を返すため、成否ではなく JSON の解釈で判断する
{ brew_outdated: 'brew outdated --json', cask_outdated: 'brew outdated --cask --json', npm_outdated: 'npm outdated -g --json 2>/dev/null', brew_list: 'brew list --versions', cask_list: 'brew list --cask --versions', npm_ls: 'npm ls -g --depth=0 --json 2>/dev/null' }.freeze
- RUBY_RELEASES_URL =
新版お知らせ(spec §1.4)の取得元。 Ruby は ruby-lang.org 公式のリリースデータを使う——rbenv install --list は 手元の ruby-build 定義依存で古い版しか返さないため使わない
'https://raw.githubusercontent.com/ruby/www.ruby-lang.org/master/_data/releases.yml'- RUBYGEMS_LATEST_URL =
'https://rubygems.org/api/v1/versions/%s/latest.json'- KIND_LABELS =
============================================================
計画の提示と確認(spec §1.1)
{ brew: 'brew', cask: 'brew cask', npm: 'npm', gem: 'gem' }.freeze
Class Method Summary collapse
-
.announce_new_versions(deps) ⇒ Object
取得失敗(タイムアウト/不通)は無言でスキップし、終了コードにも影響させない ——お知らせは付加情報であり、ツール更新本体を妨げない.
-
.apply_node_link_rule(entries) ⇒ Object
node を更新する場合は @vivliostyle/cli も必ず最新へ連動更新する(spec §1.3)。 Node 26 × vivliostyle 10.6 の Chrome 展開デッドロックのような 「バージョン組み合わせ起因の実害」を再発させないための規則。.
- .brew_entry(tool, versions, outdated) ⇒ Object
-
.brew_install_packages(missing) ⇒ Array<String>
--fix(不足分の新規インストール)が使う brew formula 名の対応表。 doctor.rb 側にパッケージ名を再列挙せず、この TOOLS を正典とする(spec §4-1)。 node は node@20 優先の特例があるため対象外(doctor.rb 側で個別処理)。.
-
.build_plan(deps) ⇒ Plan?
全ツールの計画行を生成する。npm の最新版確認に失敗(オフライン疑い)した場合は nil を返し、呼び出し元が中断する。.
-
.confirm_upgrade?(options, deps) ⇒ Boolean
更新実行の最終確認。--yes で即実行、非対話(パイプ/CI)では安全側に倒して実行しない。.
- .default_deps ⇒ Object
- .describe_entry(entry) ⇒ Object
-
.detect_ruby_route(prefix = ) ⇒ Object
起動中の Ruby の導入経路を判別する。which での存在確認ではなく RbConfig の prefix パスを見る——rbenv がインストールされていてもシステム Ruby で動いている ケースを誤案内しないため(spec §1.4)。.
-
.execute_update(entry, deps) ⇒ Object
実行と集計(spec §2.3) ============================================================.
-
.gem_entry(tool, deps) ⇒ Object
Enhanced Mode プラグイン(導入済みの場合のみ更新対象・spec §1.2)。 現在版のパース失敗・最新版の取得失敗でも更新対象に含める——gem update は 冪等で「更新して害はない」ため(spec §2.2).
-
.http_get(url, timeout: 2) ⇒ Object
タイムアウト 2 秒の HTTP GET。失敗はすべて nil(お知らせ系は無言スキップ・spec §2.2).
-
.installed_gem_version(gem_name) ⇒ Object
インストール済み gemspec の走査で現在版を得る(pdf_plugin_installed? と同じ手法)。
gem listの子プロセスは Bundler 配下(bundle exec 経由の実行)だと Gemfile に 拘束され、Gemfile に無いプラグインが見えないため使わない. - .latest_gem_version(deps, gem_name) ⇒ Object
-
.latest_ruby_patch(deps, current) ⇒ Object
ruby-lang.org 公式リリースデータから、current と同一 patch 系列の最新版を返す。 新版がない・取得やパースに失敗した場合は nil(無言スキップ)。.
- .manual_entry(tool) ⇒ Object
- .manual_recovery_command(entry) ⇒ Object
-
.npm_base_name(spec) ⇒ Object
版ピン付き npm 指定(pkg@3.0.0)からパッケージ名部分を取り出す。 スコープ付き(@vivliostyle/cli)は先頭 @ が区切りではないためそのまま返す.
- .npm_entry(tool, versions, outdated) ⇒ Object
-
.npm_upgrade_command(entry) ⇒ Object
版ピン付きパッケージ(例: textlint-rule-no-doubled-conjunction@3.0.0)はピンを維持し、 それ以外は @latest を明示して更新する.
-
.parse_brew_outdated(text, section) ⇒ Object
brew outdated --json の出力から { formula名 => [導入版, 最新版] } を作る.
-
.parse_brew_versions(text) ⇒ Object
brew list --versions("名前 版 [版...]" 形式)から { 名前 => 最新導入版 } を作る.
-
.parse_json_hash(text) ⇒ Object
JSON をハッシュとして解釈する。空文字列は {}(差分なし)、 パース不能は nil(オフライン疑いの信号)を返す.
-
.parse_npm_versions(text) ⇒ Object
npm ls -g --depth=0 --json から { パッケージ名 => 版 } を作る.
- .present_plan(plan) ⇒ Object
-
.report_failures(failed) ⇒ Object
失敗ツールの手動復旧コマンドをまとめて提示する (warning-messages の方針: 具体的な対処を必ず添える).
-
.ruby_route_lines(route, latest) ⇒ Object
経路別の更新手順。Ruby はバージョンごとに gem 領域が分かれるため、 切替後の gem 再インストール(vs が見つからなくなる事故の予防)を必ず添える.
-
.ruby_update_notice(deps, current: RUBY_VERSION, route: nil) ⇒ Array<String>
起動中の Ruby と同一 patch 系列(X.Y.*)の新版があれば、導入経路に合った 更新手順を案内する。マイナー/メジャー更新は gemspec の required_ruby_version との 整合確認を伴うため対象外(リリースノートの責務・spec §1.4).
-
.run!(options, deps: default_deps) ⇒ Integer
外部ツール更新の一連の流れ(計画提示 → 確認 → 実行 → 再診断 → 集計 → お知らせ)を統率する。.
-
.run_gem_command(cmd, deps) ⇒ Object
Bundler 配下(bundle exec 経由の実行)だと gem サブプロセスが Gemfile に拘束され、 Gemfile に無いプラグインを更新できないため、素の環境で実行する.
- .run_update_command(entry, deps) ⇒ Object
Class Method Details
.announce_new_versions(deps) ⇒ Object
取得失敗(タイムアウト/不通)は無言でスキップし、終了コードにも影響させない ——お知らせは付加情報であり、ツール更新本体を妨げない
454 455 456 457 458 459 460 461 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 454 def announce_new_versions(deps) lines = ruby_update_notice(deps) return if lines.empty? Common.log_always('') Common.log_always('📣 お知らせ:') lines.each { Common.log_always(" #{it}") } end |
.apply_node_link_rule(entries) ⇒ Object
node を更新する場合は @vivliostyle/cli も必ず最新へ連動更新する(spec §1.3)。 Node 26 × vivliostyle 10.6 の Chrome 展開デッドロックのような 「バージョン組み合わせ起因の実害」を再発させないための規則。
256 257 258 259 260 261 262 263 264 265 266 267 268 269 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 256 def apply_node_link_rule(entries) node = entries.find { it.label == 'node' } return entries unless node && node.action == :upgrade entries.map do |entry| next entry unless entry.label == 'vivliostyle CLI' case entry.action when :current then entry.with(action: :upgrade, latest: '最新', note: 'node と連動') when :upgrade then entry.with(note: 'node と連動') else entry end end end |
.brew_entry(tool, versions, outdated) ⇒ Object
271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 271 def brew_entry(tool, versions, outdated) outdated_name = outdated.keys.find { tool.matches_formula?(it) } installed_name = versions.keys.find { tool.matches_formula?(it) } if outdated_name installed, latest = outdated[outdated_name] PlanEntry.new(label: tool.label, kind: tool.kind, package: outdated_name, current: installed || versions[outdated_name], latest:, action: :upgrade, note: nil) elsif installed_name PlanEntry.new(label: tool.label, kind: tool.kind, package: installed_name, current: versions[installed_name], latest: nil, action: :current, note: nil) else PlanEntry.new(label: tool.label, kind: tool.kind, package: tool.package, current: nil, latest: nil, action: :missing, note: nil) end end |
.brew_install_packages(missing) ⇒ Array<String>
--fix(不足分の新規インストール)が使う brew formula 名の対応表。 doctor.rb 側にパッケージ名を再列挙せず、この TOOLS を正典とする(spec §4-1)。 node は node@20 優先の特例があるため対象外(doctor.rb 側で個別処理)。
348 349 350 351 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 348 def brew_install_packages(missing) TOOLS.select { it.kind == :brew && it.package != 'node' && it.checks.intersect?(missing) } .map(&:package) end |
.build_plan(deps) ⇒ Plan?
全ツールの計画行を生成する。npm の最新版確認に失敗(オフライン疑い)した場合は nil を返し、呼び出し元が中断する。
226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 226 def build_plan(deps) # --- Phase: 一括取得 --- formula_outdated = parse_brew_outdated(deps.capture.call(QUERIES[:brew_outdated]).first, 'formulae') cask_outdated = parse_brew_outdated(deps.capture.call(QUERIES[:cask_outdated]).first, 'casks') npm_outdated = parse_json_hash(deps.capture.call(QUERIES[:npm_outdated]).first) if npm_outdated.nil? Common.log_error('npm の最新版確認に失敗しました(オフライン?)。ネットワーク接続を確認して再実行してください。') return nil end formula_versions = parse_brew_versions(deps.capture.call(QUERIES[:brew_list]).first) cask_versions = parse_brew_versions(deps.capture.call(QUERIES[:cask_list]).first) npm_versions = parse_npm_versions(deps.capture.call(QUERIES[:npm_ls]).first) # --- Phase: ツールごとの行生成 --- entries = TOOLS.filter_map do |tool| case tool.kind when :brew then brew_entry(tool, formula_versions, formula_outdated) when :cask then brew_entry(tool, cask_versions, cask_outdated) when :npm then npm_entry(tool, npm_versions, npm_outdated) when :gem then gem_entry(tool, deps) when :manual then manual_entry(tool) end end Plan.new(entries: apply_node_link_rule(entries)) end |
.confirm_upgrade?(options, deps) ⇒ Boolean
更新実行の最終確認。--yes で即実行、非対話(パイプ/CI)では安全側に倒して実行しない。
379 380 381 382 383 384 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 379 def confirm_upgrade?(, deps) return true if [:yes] return false unless deps.stdin.tty? Common.confirm?('更新を実行しますか?', input: deps.stdin) end |
.default_deps ⇒ Object
149 150 151 152 153 154 155 156 157 158 159 160 161 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 149 def default_deps Deps.new( run: ->(cmd) { Kernel.system(cmd) }, capture: lambda do |cmd| out, status = Open3.capture2(cmd) [out, status.success?] rescue StandardError ['', false] end, fetch: ->(url) { http_get(url) }, stdin: $stdin ) end |
.describe_entry(entry) ⇒ Object
368 369 370 371 372 373 374 375 376 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 368 def describe_entry(entry) origin = [KIND_LABELS[entry.kind], entry.note].compact.join('・') case entry.action when :upgrade then "#{(entry.current || '不明').ljust(10)} → #{entry.latest || '最新'} (#{origin})" when :current then "#{entry.current.to_s.ljust(10)} → 変更なし (#{origin}・最新)" when :missing then "未導入(不足していれば再診断で自動導入・#{origin})" when :manual then "対象外(手動#{entry.note ? "・#{entry.note}" : ''})" end end |
.detect_ruby_route(prefix = ) ⇒ Object
起動中の Ruby の導入経路を判別する。which での存在確認ではなく RbConfig の prefix パスを見る——rbenv がインストールされていてもシステム Ruby で動いている ケースを誤案内しないため(spec §1.4)。
501 502 503 504 505 506 507 508 509 510 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 501 def detect_ruby_route(prefix = RbConfig::CONFIG['prefix']) case prefix when %r{/\.rbenv/} then :rbenv when %r{/\.rvm/} then :rvm when %r{/\.asdf/} then :asdf when %r{/mise/} then :mise when %r{\A/opt/homebrew/}, %r{\A/usr/local/Cellar/} then :homebrew else :unknown end end |
.execute_update(entry, deps) ⇒ Object
============================================================
実行と集計(spec §2.3)
390 391 392 393 394 395 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 390 def execute_update(entry, deps) Common.log_always("⬆️ #{entry.label} を更新中…") ok = run_update_command(entry, deps) Common.log_always(ok ? "✅ #{entry.label}: 更新しました" : "❌ #{entry.label}: 更新に失敗しました") UpdateResult.new(entry:, ok:) end |
.gem_entry(tool, deps) ⇒ Object
Enhanced Mode プラグイン(導入済みの場合のみ更新対象・spec §1.2)。 現在版のパース失敗・最新版の取得失敗でも更新対象に含める——gem update は 冪等で「更新して害はない」ため(spec §2.2)
311 312 313 314 315 316 317 318 319 320 321 322 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 311 def gem_entry(tool, deps) return nil unless DoctorCommands.pdf_plugin_installed? current = installed_gem_version(tool.package) latest = latest_gem_version(deps, tool.package) up_to_date = current && latest && Gem::Version.correct?(current) && Gem::Version.new(latest) <= Gem::Version.new(current) PlanEntry.new(label: tool.label, kind: :gem, package: tool.package, current: current || '不明', latest:, action: up_to_date ? :current : :upgrade, note: nil) end |
.http_get(url, timeout: 2) ⇒ Object
タイムアウト 2 秒の HTTP GET。失敗はすべて nil(お知らせ系は無言スキップ・spec §2.2)
588 589 590 591 592 593 594 595 596 597 598 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 588 def http_get(url, timeout: 2) uri = URI.parse(url) http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = uri.scheme == 'https' http.open_timeout = timeout http.read_timeout = timeout response = http.get(uri.request_uri) response.is_a?(Net::HTTPSuccess) ? response.body : nil rescue StandardError nil end |
.installed_gem_version(gem_name) ⇒ Object
インストール済み gemspec の走査で現在版を得る(pdf_plugin_installed? と同じ手法)。
gem list の子プロセスは Bundler 配下(bundle exec 経由の実行)だと Gemfile に
拘束され、Gemfile に無いプラグインが見えないため使わない
332 333 334 335 336 337 338 339 340 341 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 332 def installed_gem_version(gem_name) Gem.path .flat_map { Dir.glob(File.join(it, 'specifications', "#{gem_name}-*.gemspec")) } .filter_map { File.basename(it, '.gemspec').delete_prefix("#{gem_name}-") } .select { Gem::Version.correct?(it) } .map { Gem::Version.new(it) } .max&.to_s rescue StandardError nil end |
.latest_gem_version(deps, gem_name) ⇒ Object
527 528 529 530 531 532 533 534 535 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 527 def latest_gem_version(deps, gem_name) body = deps.fetch.call(format(RUBYGEMS_LATEST_URL, gem_name)) return nil unless body version = JSON.parse(body)['version'] Gem::Version.correct?(version) ? version : nil rescue StandardError nil end |
.latest_ruby_patch(deps, current) ⇒ Object
ruby-lang.org 公式リリースデータから、current と同一 patch 系列の最新版を返す。 新版がない・取得やパースに失敗した場合は nil(無言スキップ)。
482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 482 def latest_ruby_patch(deps, current) body = deps.fetch.call(RUBY_RELEASES_URL) return nil unless body series = current.split('.').first(2).join('.') top = YAML.safe_load(body, permitted_classes: [Date, Time], aliases: true) .filter_map { it['version'] if it.is_a?(Hash) } .select { it.is_a?(String) && it.start_with?("#{series}.") && Gem::Version.correct?(it) } .map { Gem::Version.new(it) } .reject(&:prerelease?) .max top && top > Gem::Version.new(current) ? top.to_s : nil rescue StandardError nil end |
.manual_entry(tool) ⇒ Object
324 325 326 327 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 324 def manual_entry(tool) PlanEntry.new(label: tool.label, kind: :manual, package: nil, current: nil, latest: nil, action: :manual, note: MANUAL_NOTES[tool.label]) end |
.manual_recovery_command(entry) ⇒ Object
437 438 439 440 441 442 443 444 445 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 437 def manual_recovery_command(entry) case entry in { kind: :cask, package: 'inkscape' } then 'brew reinstall --cask --force inkscape' in { kind: :cask } then "brew upgrade --cask #{entry.package}" in { kind: :brew } then "brew upgrade #{entry.package}(解決しない場合は brew doctor で環境を点検)" in { kind: :npm } then npm_upgrade_command(entry) in { kind: :gem } then "gem update #{entry.package}" end end |
.npm_base_name(spec) ⇒ Object
版ピン付き npm 指定(pkg@3.0.0)からパッケージ名部分を取り出す。 スコープ付き(@vivliostyle/cli)は先頭 @ が区切りではないためそのまま返す
585 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 585 def npm_base_name(spec) = spec.start_with?('@') ? spec : spec.split('@').first |
.npm_entry(tool, versions, outdated) ⇒ Object
287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 287 def npm_entry(tool, versions, outdated) names = tool.package == :textlint_set ? TEXTLINT_NPM_PACKAGES.map { npm_base_name(it) } : [tool.package] current = versions[names.first] outdated_name = names.find { outdated.key?(it) } info = outdated_name ? outdated[outdated_name] : nil info = nil unless info.is_a?(Hash) if outdated_name # textlint 一式は複数パッケージの束のため、単一の最新版表示はしない latest = tool.package == :textlint_set ? nil : info&.fetch('latest', nil) PlanEntry.new(label: tool.label, kind: :npm, package: tool.package, current: current || info&.fetch('current', nil), latest:, action: :upgrade, note: nil) elsif current PlanEntry.new(label: tool.label, kind: :npm, package: tool.package, current:, latest: nil, action: :current, note: nil) else PlanEntry.new(label: tool.label, kind: :npm, package: tool.package, current: nil, latest: nil, action: :missing, note: nil) end end |
.npm_upgrade_command(entry) ⇒ Object
版ピン付きパッケージ(例: textlint-rule-no-doubled-conjunction@3.0.0)はピンを維持し、 それ以外は @latest を明示して更新する
422 423 424 425 426 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 422 def npm_upgrade_command(entry) packages = entry.package == :textlint_set ? TEXTLINT_NPM_PACKAGES : [entry.package] specs = packages.map { it.match?(/.@/) ? it : "#{it}@latest" } "npm install --loglevel=error -g #{specs.map { Shellwords.escape(it) }.join(' ')}" end |
.parse_brew_outdated(text, section) ⇒ Object
brew outdated --json の出力から { formula名 => [導入版, 最新版] } を作る
554 555 556 557 558 559 560 561 562 563 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 554 def parse_brew_outdated(text, section) data = parse_json_hash(text) return {} unless data.is_a?(Hash) Array(data[section]).filter_map do |item| next unless item.is_a?(Hash) && item['name'] [item['name'], [Array(item['installed_versions']).last, item['current_version']]] end.to_h end |
.parse_brew_versions(text) ⇒ Object
brew list --versions("名前 版 [版...]" 形式)から { 名前 => 最新導入版 } を作る
566 567 568 569 570 571 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 566 def parse_brew_versions(text) text.to_s.lines.filter_map do |line| name, *versions = line.split [name, versions.last] if name && !versions.empty? end.to_h end |
.parse_json_hash(text) ⇒ Object
JSON をハッシュとして解釈する。空文字列は {}(差分なし)、 パース不能は nil(オフライン疑いの信号)を返す
543 544 545 546 547 548 549 550 551 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 543 def parse_json_hash(text) stripped = text.to_s.strip return {} if stripped.empty? data = JSON.parse(stripped) data.is_a?(Hash) ? data : nil rescue JSON::ParserError nil end |
.parse_npm_versions(text) ⇒ Object
npm ls -g --depth=0 --json から { パッケージ名 => 版 } を作る
574 575 576 577 578 579 580 581 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 574 def parse_npm_versions(text) data = parse_json_hash(text) return {} unless data.is_a?(Hash) (data['dependencies'] || {}).filter_map do |name, info| [name, info['version']] if info.is_a?(Hash) && info['version'] end.to_h end |
.present_plan(plan) ⇒ Object
359 360 361 362 363 364 365 366 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 359 def present_plan(plan) Common.log_always('📋 更新計画:') width = plan.entries.map { it.label.length }.max + 2 plan.entries.each { Common.log_always(" #{it.label.ljust(width)}#{describe_entry(it)}") } return unless plan.entries.any? { it.action == :missing } Common.log_always(' ※ 未導入のツールは、更新後の再診断で不足を確認のうえ自動インストールします(--fix 相当)') end |
.report_failures(failed) ⇒ Object
失敗ツールの手動復旧コマンドをまとめて提示する (warning-messages の方針: 具体的な対処を必ず添える)
430 431 432 433 434 435 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 430 def report_failures(failed) return if failed.empty? detail = failed.map { "#{it.entry.label}: #{manual_recovery_command(it.entry)}" }.join("\n") Common.log_warn('更新に失敗したツールがあります。以下のコマンドで手動更新を試してください:', detail:) end |
.ruby_route_lines(route, latest) ⇒ Object
経路別の更新手順。Ruby はバージョンごとに gem 領域が分かれるため、 切替後の gem 再インストール(vs が見つからなくなる事故の予防)を必ず添える
514 515 516 517 518 519 520 521 522 523 524 525 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 514 def ruby_route_lines(route, latest) gem_note = ' gem install vivlio-starter # Ruby 切替後は gem の再インストールが必要です' steps = case route when :rbenv then " brew upgrade ruby-build && rbenv install #{latest} && rbenv global #{latest}" when :rvm then " rvm install #{latest} && rvm --default use #{latest}" when :asdf then " asdf install ruby #{latest} && asdf global ruby #{latest}" when :mise then " mise use --global ruby@#{latest}" when :homebrew then ' brew upgrade ruby' else ' 導入経路を判別できませんでした。https://www.ruby-lang.org/ja/downloads/ を参照してください。' end [steps, gem_note] end |
.ruby_update_notice(deps, current: RUBY_VERSION, route: nil) ⇒ Array<String>
起動中の Ruby と同一 patch 系列(X.Y.*)の新版があれば、導入経路に合った 更新手順を案内する。マイナー/メジャー更新は gemspec の required_ruby_version との 整合確認を伴うため対象外(リリースノートの責務・spec §1.4)
467 468 469 470 471 472 473 474 475 476 477 478 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 467 def ruby_update_notice(deps, current: RUBY_VERSION, route: nil) latest = latest_ruby_patch(deps, current) return [] unless latest route ||= detect_ruby_route heading = if route == :unknown "Ruby #{latest} が公開されています(現在 #{current}):" else "Ruby #{latest} が公開されています(現在 #{current})。#{route} 環境の更新手順:" end [heading] + ruby_route_lines(route, latest) end |
.run!(options, deps: default_deps) ⇒ Integer
外部ツール更新の一連の流れ(計画提示 → 確認 → 実行 → 再診断 → 集計 → お知らせ)を統率する。
166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 166 def run!(, deps: default_deps) # --- Phase: 前提確認(--fix と同じく macOS + Homebrew のみ対応・spec §0)--- # 非対応環境はエラーではなくスキップ——vs upgrade の他フェーズ(本体更新・ # 雛形追従)はプラットフォーム非依存で、ここで全体を止める理由がない unless RbConfig::CONFIG['host_os'] =~ /darwin/i Common.log_warn('外部ツールの更新は macOS + Homebrew のみ対応のためスキップします(診断は vs doctor で実行できます)。') return 0 end unless deps.run.call('which brew >/dev/null 2>&1') Common.log_warn('Homebrew が見つからないため外部ツールの更新をスキップします。vs doctor --fix で導入するか、https://brew.sh/ を参照してください。') return 0 end # --- Phase: 最新情報の取得(オフラインなら中断——中途半端な更新をしない・spec §2.2)--- Common.log_always('🔍 外部ツールのバージョンを確認しています…') unless deps.run.call('brew update >/dev/null 2>&1') Common.log_error('最新版の確認ができません(オフライン?)。ネットワーク接続を確認して再実行してください。') return 1 end plan = build_plan(deps) return 1 unless plan # --- Phase: 計画提示 → 確認 --- present_plan(plan) if [:dry_run] Common.log_always('--dry-run のためツール更新は実行しません。') if plan.updates.any? return 0 end if plan.updates.empty? Common.log_always('✅ すべて最新です') elsif !confirm_upgrade?(, deps) Common.log_always('更新をスキップしました。--yes を付けると確認なしで実行できます。') return 0 end # --- Phase: 実行(1 ツールの失敗で全体を中断しない・spec §2.3)--- results = plan.updates.map { execute_update(it, deps) } # --- Phase: 再診断(未導入分のインストールは既存 --fix へ委譲=上位互換・spec §1.2)--- Common.log_always('🩺 更新後の診断を実行します…') rediagnosis_ok = DoctorCommands.execute_doctor( { options: { fix: true, yes: [:yes], verbose: [:verbose] } } ) # --- Phase: 集計 → 新版お知らせ --- failed = results.reject(&:ok) Common.log_always("✅ 更新完了: #{results.count(&:ok)} 件成功 / #{failed.size} 件失敗") if results.any? report_failures(failed) announce_new_versions(deps) failed.empty? && rediagnosis_ok ? 0 : 1 end |
.run_gem_command(cmd, deps) ⇒ Object
Bundler 配下(bundle exec 経由の実行)だと gem サブプロセスが Gemfile に拘束され、 Gemfile に無いプラグインを更新できないため、素の環境で実行する
416 417 418 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 416 def run_gem_command(cmd, deps) defined?(Bundler) ? Bundler.with_unbundled_env { deps.run.call(cmd) } : deps.run.call(cmd) end |
.run_update_command(entry, deps) ⇒ Object
397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 397 def run_update_command(entry, deps) case entry in { kind: :cask, package: 'inkscape' } # 半壊 cask(app 本体欠落)は通常の upgrade が失敗するため --force 再インストールへ # フォールバックする(doctor.rb の install_inkscape_macos! と同じ知見) deps.run.call('brew upgrade --cask inkscape') || deps.run.call('brew reinstall --cask --force inkscape') in { kind: :cask } then deps.run.call("brew upgrade --cask #{entry.package}") in { kind: :brew } then deps.run.call("brew upgrade #{entry.package}") in { kind: :npm } then deps.run.call(npm_upgrade_command(entry)) in { kind: :gem } then run_gem_command("gem update #{entry.package}", deps) end rescue StandardError => e Common.log_warn("#{entry.label} の更新でエラー: #{e.class}: #{e.}") false end |