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 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]), # VFM は Vivliostyle CLI に同梱されない別パッケージ(`vfm` コマンドの提供元) Tool.new(label: 'VFM', kind: :npm, package: '@vivliostyle/vfm', checks: %w[vfm]), 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 pdftotext]), 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 から { パッケージ名 => 版 } を作る.
-
.pinned_names(specs) ⇒ Array<String>
版を固定して指定したパッケージ名を取り出す(
pkg@1.2.3→pkg)。. - .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
取得失敗(タイムアウト/不通)は無言でスキップし、終了コードにも影響させない ——お知らせは付加情報であり、ツール更新本体を妨げない
474 475 476 477 478 479 480 481 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 474 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 展開デッドロックのような 「バージョン組み合わせ起因の実害」を再発させないための規則。
258 259 260 261 262 263 264 265 266 267 268 269 270 271 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 258 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
273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 273 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 側で個別処理)。
368 369 370 371 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 368 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 を返し、呼び出し元が中断する。
228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 228 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)では安全側に倒して実行しない。
399 400 401 402 403 404 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 399 def confirm_upgrade?(, deps) return true if [:yes] return false unless deps.stdin.tty? Common.confirm?('更新を実行しますか?', input: deps.stdin) end |
.default_deps ⇒ Object
151 152 153 154 155 156 157 158 159 160 161 162 163 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 151 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
388 389 390 391 392 393 394 395 396 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 388 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)。
521 522 523 524 525 526 527 528 529 530 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 521 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)
410 411 412 413 414 415 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 410 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)
331 332 333 334 335 336 337 338 339 340 341 342 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 331 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)
608 609 610 611 612 613 614 615 616 617 618 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 608 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 に無いプラグインが見えないため使わない
352 353 354 355 356 357 358 359 360 361 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 352 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
547 548 549 550 551 552 553 554 555 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 547 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(無言スキップ)。
502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 502 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
344 345 346 347 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 344 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
457 458 459 460 461 462 463 464 465 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 457 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)は先頭 @ が区切りではないためそのまま返す
605 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 605 def npm_base_name(spec) = spec.start_with?('@') ? spec : spec.split('@').first |
.npm_entry(tool, versions, outdated) ⇒ Object
304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 304 def npm_entry(tool, versions, outdated) specs = tool.package == :textlint_set ? TEXTLINT_NPM_PACKAGES : [tool.package.to_s] names = specs.map { npm_base_name(it) } pinned = pinned_names(specs) current = versions[names.first] # 固定したものは除いてから「更新できるものがあるか」を見る outdated_name = names.find { outdated.key?(it) && !pinned.include?(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 を明示して更新する
442 443 444 445 446 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 442 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名 => [導入版, 最新版] } を作る
574 575 576 577 578 579 580 581 582 583 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 574 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("名前 版 [版...]" 形式)から { 名前 => 最新導入版 } を作る
586 587 588 589 590 591 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 586 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(オフライン疑いの信号)を返す
563 564 565 566 567 568 569 570 571 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 563 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 から { パッケージ名 => 版 } を作る
594 595 596 597 598 599 600 601 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 594 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 |
.pinned_names(specs) ⇒ Array<String>
版を固定して指定したパッケージ名を取り出す(pkg@1.2.3 → pkg)。
こちらが版を決めているので、上流に新版が出ても更新対象にしない。
これを見ずに npm outdated の有無だけで判定していたため、固定した版が
毎回「古い」と報告され、更新しても状態は変わらず、vs upgrade のたびに
一式の再インストールが走り続けていた(実測 2026-08-12・約 20 秒/回)。
スコープ付き(@scope/pkg)は先頭の @ が名前の一部なので、2 文字目以降で版を切る。
299 300 301 302 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 299 def pinned_names(specs) specs.select { it[1..].to_s.include?('@') } .map { "#{it[0]}#{it[1..].split('@').first}" } end |
.present_plan(plan) ⇒ Object
379 380 381 382 383 384 385 386 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 379 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 の方針: 具体的な対処を必ず添える)
450 451 452 453 454 455 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 450 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 が見つからなくなる事故の予防)を必ず添える
534 535 536 537 538 539 540 541 542 543 544 545 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 534 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)
487 488 489 490 491 492 493 494 495 496 497 498 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 487 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
外部ツール更新の一連の流れ(計画提示 → 確認 → 実行 → 再診断 → 集計 → お知らせ)を統率する。
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 218 219 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 168 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 に無いプラグインを更新できないため、素の環境で実行する
436 437 438 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 436 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
417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 |
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 417 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 |