Module: Lich::Common::CLI::CLIOrchestration Private

Defined in:
documented/common/cli/cli_orchestration.rb

Overview

This module is part of a private API. You should avoid using this module if possible, as it may be removed or be changed in the future.

Orchestrates CLI operations: early-exit handlers for password management, data conversion, and login flow. Uses CliOptionsRegistry for declarative option registration and handler execution.

Class Method Summary collapse

Class Method Details

.check_conversion_needed_for_login ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Checks if legacy data conversion is required before a login attempt.

If conversion is needed, prints a conversion help message and exits with status code 1. This prevents login attempts when the entry store requires migration from legacy DAT format to YAML.



71
72
73
74
75
76
77
# File 'documented/common/cli/cli_orchestration.rb', line 71

def self.
  # Check if conversion is required
  if Lich::Common::CLI::CLIConversion.conversion_needed?(DATA_DIR)
    Lich::Common::CLI::CLIConversion.print_conversion_help_message
    exit 1
  end
end

.execute ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Execute registered CLI operations Processes ARGV for early-exit CLI operations (password mgmt, conversion) Also handles conversion detection for login attempts



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
# File 'documented/common/cli/cli_orchestration.rb', line 30

def self.execute
  ActiveSessionsQuery.execute

  ARGV.each do |arg|
    case arg
    when /^--change-account-password$/, /^-cap$/
      
    when /^--add-account$/, /^-aa$/
      
    when /^--refresh-characters$/, /^-rc$/
      handle_refresh_characters
    when /^--add-character$/, /^-ac$/
      handle_add_character
    when /^--change-master-password$/, /^-cmp$/
      handle_change_master_password
    when /^--recover-master-password$/, /^-rmp$/
      handle_recover_master_password
    when /^--convert-entries$/
      handle_convert_entries
    when /^--change-encryption-mode$/, /^-cem$/
      handle_change_encryption_mode
    when /^--web-login-test$/
      
    end
  end

  # Check for conversion needed before login attempt
  # This is not an early-exit operation - it detects a precondition for login
  if ARGV.include?('--login')
    
  end
end

.handle_add_account ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Adds a new account to the entry store, with optional frontend preference.

Reads account name, password, and optional --frontend flag from ARGV. If no YAML entry file exists but legacy DAT format data is present, automatically converts to plaintext YAML (with warnings) before proceeding. Exits with status 1 if required account or password arguments are missing, or if YAML creation fails. Otherwise exits via CLIPassword.add_account.

Raises:

  • (SystemExit) —

    with status 1 if account/password missing or YAML creation fails



114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'documented/common/cli/cli_orchestration.rb', line 114

def self.
  idx = ARGV.index { |a| a =~ /^--add-account$|^-aa$/ }
   = ARGV[idx + 1]
  password = ARGV[idx + 2]

  if .nil? || password.nil?
    lich_script = File.join(LICH_DIR, 'lich.rbw')
    $stdout.puts 'error: Missing required arguments'
    $stdout.puts "Usage: ruby #{lich_script} --add-account ACCOUNT PASSWORD [--frontend FRONTEND]"
    $stdout.puts "   or: ruby #{lich_script} -aa ACCOUNT PASSWORD [--frontend FRONTEND]"
    exit 1
  end

  # Check if YAML file exists; if not, check for DAT and auto-convert to plaintext
  yaml_file = Lich::Common::Authentication::EntryStore.yaml_file_path(DATA_DIR)
  unless File.exist?(yaml_file)
    if Lich::Common::CLI::CLIConversion.conversion_needed?(DATA_DIR)
      $stdout.puts ''
      $stdout.puts '=' * 80
      $stdout.puts 'WARNING: No YAML entry file found. Legacy entry.dat detected.'
      $stdout.puts 'Creating plaintext YAML file to support this operation.'
      $stdout.puts '=' * 80
      $stdout.puts ''
      $stdout.puts 'SECURITY NOTICE: Plaintext storage is not recommended except for'
      $stdout.puts 'accessibility requirements. Passwords will be stored in clear text.'
      $stdout.puts ''
      $stdout.puts 'To upgrade encryption later, use:'
      $stdout.puts "  ruby #{File.join(LICH_DIR, 'lich.rbw')} --change-encryption-mode enhanced"
      $stdout.puts '  or: -cem enhanced'
      $stdout.puts '=' * 80
      $stdout.puts ''

      # Perform plaintext conversion
      success = Lich::Common::CLI::CLIConversion.convert(DATA_DIR, :plaintext)
      unless success
        $stdout.puts 'error: Failed to create YAML file from legacy data.'
        exit 1
      end
      # Continue with add-account operation below
    end
    # No DAT file either - CLIPassword.add_account will create new YAML
  end

  frontend = ARGV[ARGV.index('--frontend') + 1] if ARGV.include?('--frontend')
  exit Lich::Common::Authentication::CLIPassword.(, password, frontend)
end

.handle_add_character ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Adds a new character to an account in the entry store.

Reads account name, character name, and required --game-code flag from ARGV. Optional --frontend flag may also be provided. Validates all positional arguments and --game-code (which must be a valid game code). Exits via CLIPassword.add_character on success or validation error.



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
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# File 'documented/common/cli/cli_orchestration.rb', line 194

def self.handle_add_character
  idx = ARGV.index { |a| a =~ /^--add-character$|^-ac$/ }
   = ARGV[idx + 1]
  char_name = ARGV[idx + 2]

  lich_script = File.join(LICH_DIR, 'lich.rbw')
  usage = "Usage: ruby #{lich_script} --add-character ACCOUNT CHAR_NAME --game-code CODE [--frontend FRONTEND]\n" \
          "   or: ruby #{lich_script} -ac ACCOUNT CHAR_NAME --game-code CODE [--frontend FRONTEND]"

   = CliOptionValidator.require_positional(, name: 'ACCOUNT', usage: usage)
  char_name = CliOptionValidator.require_positional(char_name, name: 'CHAR_NAME', usage: usage)

  game_code = CliOptionValidator.extract_flag_value('--game-code', usage: usage)
  if game_code.nil?
    $stdout.puts 'error: --game-code is required'
    $stdout.puts usage
    exit 1
  end

  unless Lich::Common::Authentication::LoginHelpers.valid_game_code?(game_code)
    CliOptionValidator.reject_invalid_value(
      '--game-code',
      game_code,
      valid_values: Lich::Common::Authentication::LoginHelpers::VALID_GAME_CODES,
      usage: usage
    )
  end

  frontend = CliOptionValidator.extract_flag_value(
    '--frontend',
    usage: usage,
    valid_values: Lich::Common::Authentication::LoginHelpers::VALID_FRONTENDS
  )

  exit Lich::Common::Authentication::CLIPassword.add_character(
    ,
    char_name,
    game_code: game_code,
    frontend: frontend
  )
end

.handle_change_account_password ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Changes the password for an existing account in the entry store.

Reads account name and new password from ARGV (--change-account-password or -cap followed by account and password arguments). Validates both arguments are present and exits with status 1 if either is missing. On success or error, exits the process via CLIPassword.change_account_password.

Raises:

  • (SystemExit) —

    with status 1 if account or password argument is missing



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'documented/common/cli/cli_orchestration.rb', line 88

def self.
  idx = ARGV.index { |a| a =~ /^--change-account-password$|^-cap$/ }
   = ARGV[idx + 1]
  new_password = ARGV[idx + 2]

  if .nil? || new_password.nil?
    lich_script = File.join(LICH_DIR, 'lich.rbw')
    $stdout.puts 'error: Missing required arguments'
    $stdout.puts "Usage: ruby #{lich_script} --change-account-password ACCOUNT NEWPASSWORD"
    $stdout.puts "   or: ruby #{lich_script} -cap ACCOUNT NEWPASSWORD"
    exit 1
  end

  exit Lich::Common::Authentication::CLIPassword.(, new_password)
end

.handle_change_encryption_mode ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Changes the encryption mode of the entry store and optionally updates the master password.

Reads encryption mode (required: plaintext, standard, or enhanced) and optional --master-password or -mp flag from ARGV. Exits with status 1 if encryption mode is missing. Otherwise exits via EncryptionModeChange.change_mode.

Raises:

  • (SystemExit) —

    with status 1 if encryption mode argument is missing



345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
# File 'documented/common/cli/cli_orchestration.rb', line 345

def self.handle_change_encryption_mode
  idx = ARGV.index { |a| a =~ /^--change-encryption-mode$|^-cem$/ }
  mode_arg = ARGV[idx + 1]

  if mode_arg.nil?
    lich_script = File.join(LICH_DIR, 'lich.rbw')
    $stdout.puts 'error: Missing encryption mode'
    $stdout.puts "Usage: ruby #{lich_script} --change-encryption-mode MODE [--master-password PASSWORD]"
    $stdout.puts "       ruby #{lich_script} -cem MODE [-mp PASSWORD]"
    $stdout.puts 'Modes: plaintext, standard, enhanced'
    exit 1
  end

  new_mode = mode_arg.to_sym

  # Check for optional master password (for Enhanced mode, if automating)
  mp_index = ARGV.index('--master-password') || ARGV.index('-mp')
  master_password = ARGV[mp_index + 1] if mp_index

  exit Lich::Common::CLI::EncryptionModeChange.change_mode(new_mode, master_password)
end

.handle_change_master_password ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Changes the master password for the entry store.

Reads old password (required) and new password (optional) from ARGV. If new password is not provided on command line, the user will be prompted for confirmation. Exits with status 1 if old password is missing. Otherwise exits via CLIPassword.change_master_password.

Raises:

  • (SystemExit) —

    with status 1 if old password argument is missing



245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'documented/common/cli/cli_orchestration.rb', line 245

def self.handle_change_master_password
  idx = ARGV.index { |a| a =~ /^--change-master-password$|^-cmp$/ }
  old_password = ARGV[idx + 1]
  new_password = ARGV[idx + 2]

  if old_password.nil?
    lich_script = File.join(LICH_DIR, 'lich.rbw')
    $stdout.puts 'error: Missing required arguments'
    $stdout.puts "Usage: ruby #{lich_script} --change-master-password OLDPASSWORD [NEWPASSWORD]"
    $stdout.puts "   or: ruby #{lich_script} -cmp OLDPASSWORD [NEWPASSWORD]"
    $stdout.puts 'Note: If NEWPASSWORD is not provided, you will be prompted for confirmation'
    exit 1
  end

  exit Lich::Common::Authentication::CLIPassword.change_master_password(old_password, new_password)
end

.handle_convert_entries ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Converts the entry store to a new encryption mode (plaintext, standard, or enhanced).

Reads encryption mode from ARGV (required: plaintext, standard, or enhanced). For enhanced mode, prompts for a new master password and stores it in the keychain before conversion. Exits with status 1 if encryption mode is missing or invalid, or if enhanced-mode master password creation or keychain storage fails. On success, exits with status 0 after printing confirmation.

Raises:

  • (SystemExit) —

    with status 1 if mode is missing/invalid or master password setup fails



287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
# File 'documented/common/cli/cli_orchestration.rb', line 287

def self.handle_convert_entries
  idx = ARGV.index('--convert-entries')
  encryption_mode_str = ARGV[idx + 1]

  if encryption_mode_str.nil?
    lich_script = File.join(LICH_DIR, 'lich.rbw')
    $stdout.puts 'error: Missing required argument'
    $stdout.puts "Usage: ruby #{lich_script} --convert-entries [plaintext|standard|enhanced]"
    exit 1
  end

  unless %w[plaintext standard enhanced].include?(encryption_mode_str)
    $stdout.puts "error: Invalid encryption mode: #{encryption_mode_str}"
    $stdout.puts 'Valid modes: plaintext, standard, enhanced'
    exit 1
  end

  # For enhanced mode, prompt for master password and store in keychain before conversion
  # This way migrate_from_legacy will find it in keychain and not try to show GUI dialog
  if encryption_mode_str == 'enhanced'
    master_password = Lich::Common::Authentication::CLIPassword.prompt_and_confirm_password('Enter new master password for enhanced encryption')
    if master_password.nil?
      puts 'error: Master password creation cancelled'
      exit 1
    end

    # Store password in keychain so ensure_master_password_exists finds it
    require_relative '../gui/master_password_manager'
    stored = Lich::Common::GUI::MasterPasswordManager.store_master_password(master_password)
    unless stored
      puts 'error: Failed to store master password in keychain'
      exit 1
    end
  end

  # Perform conversion
  success = Lich::Common::CLI::CLIConversion.convert(
    DATA_DIR,
    encryption_mode_str
  )

  if success
    $stdout.puts 'Conversion completed successfully!'
    exit 0
  else
    $stdout.puts 'Conversion failed. Please check the logs for details.'
    exit 1
  end
end

.handle_recover_master_password ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Recovers access to the entry store by setting a new master password.

Reads optional new password from ARGV. If new password is not provided on command line, the user will be prompted interactively. Exits via CLIPassword.recover_master_password.



269
270
271
272
273
274
275
# File 'documented/common/cli/cli_orchestration.rb', line 269

def self.handle_recover_master_password
  idx = ARGV.index { |a| a =~ /^--recover-master-password$|^-rmp$/ }
  new_password = ARGV[idx + 1]

  # new_password is optional - if not provided, user will be prompted interactively
  exit Lich::Common::Authentication::CLIPassword.recover_master_password(new_password)
end

.handle_refresh_characters ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Refreshes the character list for an account from the game server.

Reads account name and optional --frontend flag from ARGV. Validates account name is provided; exits with validation error if missing. Validates frontend (if supplied) against known frontends. Exits via CLIPassword.refresh_characters.



168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'documented/common/cli/cli_orchestration.rb', line 168

def self.handle_refresh_characters
  idx = ARGV.index { |a| a =~ /^--refresh-characters$|^-rc$/ }
   = ARGV[idx + 1]

  lich_script = File.join(LICH_DIR, 'lich.rbw')
  usage = "Usage: ruby #{lich_script} --refresh-characters ACCOUNT [--frontend FRONTEND]\n" \
          "   or: ruby #{lich_script} -rc ACCOUNT [--frontend FRONTEND]"

   = CliOptionValidator.require_positional(, name: 'ACCOUNT', usage: usage)

  frontend = CliOptionValidator.extract_flag_value(
    '--frontend',
    usage: usage,
    valid_values: Lich::Common::Authentication::LoginHelpers::VALID_FRONTENDS
  )
  exit Lich::Common::Authentication::CLIPassword.refresh_characters(, frontend)
end

.handle_web_login_test ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Standalone probe of the HTTPS web-login fallback path (see docs/web-login-protocol-analysis.md). Exercises WebLogin.auth_with_timeout directly against play.net -- independent of the real login path (Authenticator.authenticate), which also uses WebLogin, either forced via --auth-provider=web or automatically as a fallback when EAccess is unreachable.



375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
# File 'documented/common/cli/cli_orchestration.rb', line 375

def self.
  idx = ARGV.index('--web-login-test')
   = ARGV[idx + 1]
  char_name = ARGV[idx + 2]

  lich_script = File.join(LICH_DIR, 'lich.rbw')
  usage = "Usage: ruby #{lich_script} --web-login-test ACCOUNT CHAR_NAME --game-code CODE\n" \
          "Reads the account's password from data/entry.yaml (ACCOUNT must already be saved there).\n" \
          'This is a standalone probe of the HTTPS web-login fallback path -- it does NOT ' \
          'touch the normal EAccess login flow.'

   = CliOptionValidator.require_positional(, name: 'ACCOUNT', usage: usage)
  char_name = CliOptionValidator.require_positional(char_name, name: 'CHAR_NAME', usage: usage)

  game_code = CliOptionValidator.extract_flag_value(
    '--game-code',
    usage: usage,
    valid_values: Lich::Common::Authentication::LoginHelpers::VALID_GAME_CODES
  )
  if game_code.nil?
    $stdout.puts 'error: --game-code is required'
    $stdout.puts usage
    exit 1
  end

  entries = Lich::Common::Authentication::EntryStore.load_saved_entries(DATA_DIR, false)
  entry = entries.find { |e| e[:user_id].to_s.casecmp?() }
  if entry.nil?
    $stdout.puts "error: Account '#{}' not found in #{Lich::Common::Authentication::EntryStore.yaml_file_path(DATA_DIR)}"
    exit 1
  end

  $stdout.puts "Probing web login fallback for #{} / #{char_name} (#{game_code})..."

  begin
     = Lich::Common::Authentication::WebLogin.auth_with_timeout(
      account: ,
      password: entry[:password],
      character: char_name,
      game_code: game_code
    )
    $stdout.puts 'Success:'
    # KEY is a live, usable one-time game-server credential -- printing
    # it would leave a real secret in terminal scrollback/log capture
    # for a probe that never consumes it. Only non-secret connection
    # metadata is shown.
    .each { |k, v| $stdout.puts "  #{k.upcase}=#{k == 'key' ? '[scrubbed]' : v}" }
    exit 0
  rescue Lich::Common::Authentication::WebLogin::AuthenticationError => e
    $stdout.puts "error: web login failed: #{e.error_code}"
    exit 1
  rescue StandardError => e
    $stdout.puts "error: #{e.class}: #{e.message}"
    exit 1
  end
end