Class: Rubino::CLI::SetupCommand

Inherits:
Object
  • Object
show all
Defined in:
lib/rubino/cli/setup_command.rb

Overview

Handles initial setup: creates config directory, default config, initializes the database, and runs migrations.

Instance Method Summary collapse

Instance Method Details

#executeObject



10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/rubino/cli/setup_command.rb', line 10

def execute
  ui = Rubino.ui

  ui.info("Setting up rubino...")
  ui.blank_line

  # Create home directory (0700 — only the owner sees stored secrets)
  # and subdirectories. ensure_directories! owns the mkdir + chmod so
  # every entry point that materializes the home agrees on 0700 (#65).
  home = Rubino.home_path
  Rubino.ensure_directories!
  ui.success("Home directory: #{home}")
  ui.success("Subdirectories created")

  # Create config file if it doesn't exist
  loader = Config::Loader.new
  if loader.config_exists?
    ui.warning("Config already exists: #{loader.config_path}")
  else
    loader.create_default_config!
    File.chmod(0o600, loader.config_path)
    ui.success("Config created: #{loader.config_path}")
  end

  # Create .env template if it doesn't exist (0600 — contains api keys)
  env_path = File.join(home, ".env")
  unless File.exist?(env_path)
    File.write(env_path, env_template)
    File.chmod(0o600, env_path)
    ui.success("Env template created: #{env_path}")
  end

  # Initialize database. `setup` is the documented remedy for a broken
  # install, so it must SELF-HEAL a corrupt/truncated DB instead of
  # crashing with a raw SQLite3::CorruptException backtrace (HIGH-2): if
  # the file is present but unopenable, quarantine it aside (preserving the
  # bytes for forensics) and recreate a fresh one.
  ui.status("Initializing database...")
  connection = recover_corrupt_database(ui)
  migrator = Database::Migrator.new(connection)
  # `setup` is the documented repair path. The schema is a single,
  # fully-idempotent baseline migration (`create_table?` / `IF NOT EXISTS`
  # throughout) applied under the cross-process flock, so this is safe to
  # run on a fresh, partial, or already-migrated home alike — a healthy DB
  # just no-ops, a partial one finishes, and a re-run never collides.
  migrator.migrate!(lock_path: Rubino.migration_lock_path)
  ui.success("Database initialized: #{connection.db_path}")

  # First-run onboarding: if no usable key is configured yet AND we're on
  # a real TTY, guide the user to a working model (provider/model/key)
  # right here so `setup` ends in a usable config — not a dead-end that
  # still needs hand-editing config.yml (#93). Non-interactive setup keeps
  # the old behaviour (files created, no prompts).
  maybe_run_onboarding(ui)

  # Non-interactive provider auto-detect (#392a): a headless `setup` can't
  # prompt, so the seeded default (openai/gpt-4.1 → OPENAI_API_KEY) is a
  # dead end when the only key in the env is, say, MINIMAX_API_KEY — doctor
  # then fails "No credentials found for provider 'openai'". When EXACTLY
  # ONE provider's key is present in the env, point model.provider /
  # model.default (and any required providers.<name> block) at it so a
  # CI/container `setup` lands on a usable config. Ambiguous (>1 key) or
  # none keeps the seeded default untouched.
  maybe_autodetect_provider(ui)

  ui.blank_line
  # Tell the truth about the end state (#31). A green "Setup complete!" is
  # only honest when a usable credential is actually configured — printing
  # it after a skipped/abandoned onboarding (no provider, no key) directly
  # contradicts the state. Re-check the credential after onboarding so the
  # final line reflects reality on both the interactive and the
  # non-interactive (files-only) paths.
  if LLM::CredentialCheck.usable?
    ui.success("Setup complete! Run 'rubino doctor' to verify.")
  elsif (model = Rubino.configuration.model_default.to_s).empty?
    ui.warning("Setup files created, but no model is configured yet.")
    ui.status("Run 'rubino setup' again or add an API key, then 'rubino doctor' to verify.")
  else
    # A model IS configured (#31) — what's missing is its CREDENTIAL (only a
    # different provider's key is present), so the old "no model configured"
    # copy was wrong. Name the model and point at the guided setup.
    provider = LLM::CredentialCheck.resolved_provider
    ui.warning("Setup files created, but the API key for #{model} (provider '#{provider}') is missing.")
    ui.status("Run 'rubino setup' to add it, then 'rubino doctor' to verify.")
  end
end