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

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

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 側で個別処理)。

Parameters:

  • missing (Array<String>)

    doctor 診断ラベルの不足一覧

Returns:

  • (Array<String>)

    インストールすべき brew formula 名



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 を返し、呼び出し元が中断する。

Returns:



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)では安全側に倒して実行しない。

Returns:

  • (Boolean)


379
380
381
382
383
384
# File 'lib/vivlio_starter/cli/doctor/tool_upgrader.rb', line 379

def confirm_upgrade?(options, deps)
  return true if options[:yes]
  return false unless deps.stdin.tty?

  Common.confirm?('更新を実行しますか?', input: deps.stdin)
end

.default_depsObject



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)

Returns:

  • (Array<String>)

    お知らせ行(新版がなければ空)



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

外部ツール更新の一連の流れ(計画提示 → 確認 → 実行 → 再診断 → 集計 → お知らせ)を統率する。

Parameters:

  • options (Hash)

    { yes:, dry_run:, verbose:, ... }

Returns:

  • (Integer)

    終了コード(全成功+再診断 OK / スキップ → 0、失敗あり/再診断 NG/中断 → 1)



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!(options, 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 options[:dry_run]
    Common.log_always('--dry-run のためツール更新は実行しません。') if plan.updates.any?
    return 0
  end
  if plan.updates.empty?
    Common.log_always('✅ すべて最新です')
  elsif !confirm_upgrade?(options, 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: options[:yes], verbose: options[: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.message}")
  false
end