legionio 1.4.59 → 1.4.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -0
- data/CHANGELOG.md +5 -0
- data/CLAUDE.md +2 -2
- data/lib/legion/api/auth_kerberos.rb +79 -0
- data/lib/legion/api/middleware/auth.rb +76 -5
- data/lib/legion/api.rb +2 -0
- data/lib/legion/cli/auth_command.rb +84 -0
- data/lib/legion/cli/chat_command.rb +1 -1
- data/lib/legion/cli/tty_command.rb +117 -0
- data/lib/legion/cli.rb +4 -0
- data/lib/legion/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d0b17b55ad4c040c0b2f9e4d23d811b2611791b930dfbf434c5ed99136bae3f6
|
|
4
|
+
data.tar.gz: 7ca5c1823f303b3f54096c5c342c067923bb17ed90e6c891bdff55825209d1b4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c77d435c1fdb2ce380d06d7c36e7099e96f002d3036579e35a49c8c4ce4de419f160dbef83244f529b86cdba7591afe5eea43464943f596c37fdf3a0ee87a16f
|
|
7
|
+
data.tar.gz: 7f31653651939165a61d378d4aa0ecf7cc1db427e922288bb7604bafaf4493828c55b4548618b252fcf0000bb54d84b63922759ebf8dd41facfa0eba067c4e3f
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
data/CLAUDE.md
CHANGED
|
@@ -9,7 +9,7 @@ The primary gem for the LegionIO framework. An extensible async job engine for s
|
|
|
9
9
|
|
|
10
10
|
**GitHub**: https://github.com/LegionIO/LegionIO
|
|
11
11
|
**Gem**: `legionio`
|
|
12
|
-
**Version**: 1.4.
|
|
12
|
+
**Version**: 1.4.60
|
|
13
13
|
**License**: Apache-2.0
|
|
14
14
|
**Docker**: `legionio/legion`
|
|
15
15
|
**Ruby**: >= 3.4
|
|
@@ -711,7 +711,7 @@ rack-test, rake, rspec, rubocop, rubocop-rspec, simplecov
|
|
|
711
711
|
|
|
712
712
|
```bash
|
|
713
713
|
bundle install
|
|
714
|
-
bundle exec rspec #
|
|
714
|
+
bundle exec rspec # 1357 examples, 0 failures
|
|
715
715
|
bundle exec rubocop # 396 files, 0 offenses
|
|
716
716
|
```
|
|
717
717
|
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
class API < Sinatra::Base
|
|
5
|
+
module Routes
|
|
6
|
+
module AuthKerberos
|
|
7
|
+
def self.registered(app)
|
|
8
|
+
register_negotiate(app)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.resolve_kerberos_role_map
|
|
12
|
+
return {} unless defined?(Legion::Settings)
|
|
13
|
+
|
|
14
|
+
Legion::Settings.dig(:kerberos, :role_map) || {}
|
|
15
|
+
rescue StandardError
|
|
16
|
+
{}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.kerberos_available?
|
|
20
|
+
defined?(Legion::Extensions::Kerberos::Client) &&
|
|
21
|
+
defined?(Legion::Rbac::KerberosClaimsMapper)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.register_negotiate(app)
|
|
25
|
+
app.get '/api/auth/negotiate' do
|
|
26
|
+
auth_header = request.env['HTTP_AUTHORIZATION']
|
|
27
|
+
|
|
28
|
+
unless auth_header&.match?(/\ANegotiate\s+/i)
|
|
29
|
+
headers['WWW-Authenticate'] = 'Negotiate'
|
|
30
|
+
halt 401, json_error('negotiate_required', 'Negotiate token required', status_code: 401)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
halt 501, json_error('kerberos_not_available', 'Kerberos extension is not loaded', status_code: 501) unless Routes::AuthKerberos.kerberos_available?
|
|
34
|
+
|
|
35
|
+
token = auth_header.sub(/\ANegotiate\s+/i, '')
|
|
36
|
+
|
|
37
|
+
auth_result = begin
|
|
38
|
+
client = Legion::Extensions::Kerberos::Client.new
|
|
39
|
+
client.authenticate(token: token)
|
|
40
|
+
rescue StandardError
|
|
41
|
+
nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
unless auth_result&.dig(:success)
|
|
45
|
+
headers['WWW-Authenticate'] = 'Negotiate'
|
|
46
|
+
halt 401, json_error('kerberos_auth_failed', 'Kerberos authentication failed', status_code: 401)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
role_map = Routes::AuthKerberos.resolve_kerberos_role_map
|
|
50
|
+
mapped = Legion::Rbac::KerberosClaimsMapper.map_with_fallback(
|
|
51
|
+
principal: auth_result[:principal],
|
|
52
|
+
groups: auth_result[:groups] || [],
|
|
53
|
+
role_map: role_map
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
ttl = 28_800
|
|
57
|
+
legion_token = Legion::API::Token.issue_human_token(
|
|
58
|
+
msid: mapped[:sub], name: mapped[:name], roles: mapped[:roles], ttl: ttl
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
output_token = auth_result[:output_token]
|
|
62
|
+
headers['WWW-Authenticate'] = "Negotiate #{output_token}" if output_token
|
|
63
|
+
|
|
64
|
+
json_response({
|
|
65
|
+
token: legion_token,
|
|
66
|
+
principal: auth_result[:principal],
|
|
67
|
+
roles: mapped[:roles],
|
|
68
|
+
auth_method: 'kerberos'
|
|
69
|
+
})
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
class << self
|
|
74
|
+
private :register_negotiate
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -4,11 +4,12 @@ module Legion
|
|
|
4
4
|
class API < Sinatra::Base
|
|
5
5
|
module Middleware
|
|
6
6
|
class Auth
|
|
7
|
-
SKIP_PATHS
|
|
8
|
-
|
|
9
|
-
AUTH_HEADER
|
|
10
|
-
BEARER_PATTERN
|
|
11
|
-
|
|
7
|
+
SKIP_PATHS = %w[/api/health /api/ready /api/openapi.json /metrics /api/auth/token /api/auth/worker-token
|
|
8
|
+
/api/auth/authorize /api/auth/callback /api/auth/negotiate].freeze
|
|
9
|
+
AUTH_HEADER = 'HTTP_AUTHORIZATION'
|
|
10
|
+
BEARER_PATTERN = /\ABearer\s+(.+)\z/i
|
|
11
|
+
NEGOTIATE_PATTERN = /\ANegotiate\s+(.+)\z/i
|
|
12
|
+
API_KEY_HEADER = 'HTTP_X_API_KEY'
|
|
12
13
|
|
|
13
14
|
def initialize(app, opts = {})
|
|
14
15
|
@app = app
|
|
@@ -21,6 +22,10 @@ module Legion
|
|
|
21
22
|
return @app.call(env) unless @enabled
|
|
22
23
|
return @app.call(env) if skip_path?(env['PATH_INFO'])
|
|
23
24
|
|
|
25
|
+
# Try Negotiate/SPNEGO first (Kerberos)
|
|
26
|
+
result = try_negotiate(env)
|
|
27
|
+
return result if result
|
|
28
|
+
|
|
24
29
|
# Try Bearer JWT first
|
|
25
30
|
token = extract_token(env)
|
|
26
31
|
if token
|
|
@@ -54,10 +59,76 @@ module Legion
|
|
|
54
59
|
|
|
55
60
|
private
|
|
56
61
|
|
|
62
|
+
def try_negotiate(env)
|
|
63
|
+
negotiate_token = extract_negotiate_token(env)
|
|
64
|
+
return nil unless negotiate_token
|
|
65
|
+
|
|
66
|
+
negotiate_result = verify_negotiate(negotiate_token)
|
|
67
|
+
unless negotiate_result
|
|
68
|
+
return kerberos_available? ? unauthorized('Kerberos authentication failed') : nil
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
env['legion.auth'] = negotiate_result[:claims]
|
|
72
|
+
env['legion.auth_method'] = 'kerberos'
|
|
73
|
+
env['legion.owner_msid'] = negotiate_result[:claims][:sub]
|
|
74
|
+
status, app_headers, body = @app.call(env)
|
|
75
|
+
response_headers = app_headers.dup
|
|
76
|
+
response_headers['WWW-Authenticate'] = "Negotiate #{negotiate_result[:output_token]}" if negotiate_result[:output_token]
|
|
77
|
+
[status, response_headers, body]
|
|
78
|
+
end
|
|
79
|
+
|
|
57
80
|
def skip_path?(path)
|
|
58
81
|
SKIP_PATHS.any? { |p| path.start_with?(p) }
|
|
59
82
|
end
|
|
60
83
|
|
|
84
|
+
def extract_negotiate_token(env)
|
|
85
|
+
header = env[AUTH_HEADER]
|
|
86
|
+
return nil unless header
|
|
87
|
+
|
|
88
|
+
match = header.match(NEGOTIATE_PATTERN)
|
|
89
|
+
match&.captures&.first
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def verify_negotiate(token)
|
|
93
|
+
return nil unless kerberos_available?
|
|
94
|
+
|
|
95
|
+
client = Legion::Extensions::Kerberos::Client.new
|
|
96
|
+
auth_result = client.authenticate(token: token)
|
|
97
|
+
return nil unless auth_result[:success]
|
|
98
|
+
|
|
99
|
+
claims = Legion::Rbac::KerberosClaimsMapper.map_with_fallback(
|
|
100
|
+
principal: auth_result[:principal],
|
|
101
|
+
groups: auth_result[:groups] || [],
|
|
102
|
+
role_map: kerberos_role_map,
|
|
103
|
+
fallback: kerberos_fallback
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
{ claims: claims, output_token: auth_result[:output_token] }
|
|
107
|
+
rescue StandardError
|
|
108
|
+
nil
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def kerberos_available?
|
|
112
|
+
defined?(Legion::Extensions::Kerberos::Client) &&
|
|
113
|
+
defined?(Legion::Rbac::KerberosClaimsMapper)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def kerberos_role_map
|
|
117
|
+
return {} unless defined?(Legion::Settings)
|
|
118
|
+
|
|
119
|
+
Legion::Settings.dig(:kerberos, :role_map) || {}
|
|
120
|
+
rescue StandardError
|
|
121
|
+
{}
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def kerberos_fallback
|
|
125
|
+
return :entra unless defined?(Legion::Settings)
|
|
126
|
+
|
|
127
|
+
Legion::Settings.dig(:kerberos, :fallback) || :entra
|
|
128
|
+
rescue StandardError
|
|
129
|
+
:entra
|
|
130
|
+
end
|
|
131
|
+
|
|
61
132
|
def extract_api_key(env)
|
|
62
133
|
env[API_KEY_HEADER]
|
|
63
134
|
end
|
data/lib/legion/api.rb
CHANGED
|
@@ -29,6 +29,7 @@ require_relative 'api/rbac'
|
|
|
29
29
|
require_relative 'api/auth'
|
|
30
30
|
require_relative 'api/auth_worker'
|
|
31
31
|
require_relative 'api/auth_human'
|
|
32
|
+
require_relative 'api/auth_kerberos'
|
|
32
33
|
require_relative 'api/capacity'
|
|
33
34
|
require_relative 'api/audit'
|
|
34
35
|
require_relative 'api/metrics'
|
|
@@ -103,6 +104,7 @@ module Legion
|
|
|
103
104
|
register Routes::Auth
|
|
104
105
|
register Routes::AuthWorker
|
|
105
106
|
register Routes::AuthHuman
|
|
107
|
+
register Routes::AuthKerberos
|
|
106
108
|
register Routes::Capacity
|
|
107
109
|
register Routes::Audit
|
|
108
110
|
register Routes::Metrics
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'thor'
|
|
4
|
+
require 'uri'
|
|
5
|
+
require 'fileutils'
|
|
4
6
|
|
|
5
7
|
module Legion
|
|
6
8
|
module CLI
|
|
@@ -72,6 +74,38 @@ module Legion
|
|
|
72
74
|
out.json({ authenticated: true, scopes: scopes, expires_in: body['expires_in'] })
|
|
73
75
|
end
|
|
74
76
|
|
|
77
|
+
desc 'kerberos', 'Authenticate using Kerberos TGT from your workstation'
|
|
78
|
+
method_option :api_url, type: :string, desc: 'Legion API base URL'
|
|
79
|
+
method_option :realm, type: :string, desc: 'Kerberos realm override'
|
|
80
|
+
def kerberos
|
|
81
|
+
klist_output = `klist 2>&1`
|
|
82
|
+
unless $CHILD_STATUS&.success?
|
|
83
|
+
say 'No Kerberos ticket found. Run kinit first or check your domain connection.', :red
|
|
84
|
+
return
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
principal_match = klist_output.match(/Principal:\s+(\S+)/)
|
|
88
|
+
unless principal_match
|
|
89
|
+
say 'Could not detect Kerberos principal from klist output.', :red
|
|
90
|
+
return
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
principal = principal_match[1]
|
|
94
|
+
realm = options[:realm] || principal.split('@', 2).last
|
|
95
|
+
say 'Detected Kerberos ticket:', :green
|
|
96
|
+
say " Principal: #{principal}"
|
|
97
|
+
say " Realm: #{realm}"
|
|
98
|
+
|
|
99
|
+
api_url = resolve_api_url
|
|
100
|
+
say "Authenticating to #{api_url}..."
|
|
101
|
+
|
|
102
|
+
token = build_spnego_token(api_url)
|
|
103
|
+
response = send_negotiate_request(api_url, token)
|
|
104
|
+
handle_negotiate_response(response)
|
|
105
|
+
rescue StandardError => e
|
|
106
|
+
say "Kerberos auth error: #{e.message}", :red
|
|
107
|
+
end
|
|
108
|
+
|
|
75
109
|
default_task :teams
|
|
76
110
|
|
|
77
111
|
no_commands do
|
|
@@ -81,6 +115,56 @@ module Legion
|
|
|
81
115
|
color: !options[:no_color]
|
|
82
116
|
)
|
|
83
117
|
end
|
|
118
|
+
|
|
119
|
+
def resolve_api_url
|
|
120
|
+
url = options[:api_url]
|
|
121
|
+
url ||= Legion::Settings.dig(:api, :url) if defined?(Legion::Settings)
|
|
122
|
+
url || 'http://127.0.0.1:4567'
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def build_spnego_token(api_url)
|
|
126
|
+
require 'gssapi'
|
|
127
|
+
require 'base64'
|
|
128
|
+
host = ::URI.parse(api_url).host
|
|
129
|
+
spnego = GSSAPI::Simple.new(host, 'HTTP')
|
|
130
|
+
::Base64.strict_encode64(spnego.init_context)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def send_negotiate_request(api_url, token)
|
|
134
|
+
require 'net/http'
|
|
135
|
+
uri = ::URI.parse("#{api_url}/api/auth/negotiate")
|
|
136
|
+
http = ::Net::HTTP.new(uri.host, uri.port)
|
|
137
|
+
request = ::Net::HTTP::Get.new(uri.request_uri)
|
|
138
|
+
request['Authorization'] = "Negotiate #{token}"
|
|
139
|
+
http.request(request)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def handle_negotiate_response(response)
|
|
143
|
+
if response.code.to_i == 200
|
|
144
|
+
body = ::JSON.parse(response.body) rescue {} # rubocop:disable Style/RescueModifier
|
|
145
|
+
token_val = body.is_a?(Hash) ? (body['token'] || body.dig('data', 'token')) : nil
|
|
146
|
+
if token_val
|
|
147
|
+
save_credentials(token_val)
|
|
148
|
+
roles = body['roles'] || body.dig('data', 'roles') || []
|
|
149
|
+
say " Roles: #{Array(roles).join(', ')}", :green
|
|
150
|
+
say ' Token saved to ~/.legionio/credentials', :green
|
|
151
|
+
say 'Login successful (kerberos)', :green
|
|
152
|
+
else
|
|
153
|
+
say 'Authentication succeeded but no token in response', :yellow
|
|
154
|
+
end
|
|
155
|
+
else
|
|
156
|
+
say "Authentication failed: HTTP #{response.code}", :red
|
|
157
|
+
say response.body.to_s, :red
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def save_credentials(token_val)
|
|
162
|
+
credentials_dir = ::File.join(::Dir.home, '.legionio')
|
|
163
|
+
::FileUtils.mkdir_p(credentials_dir)
|
|
164
|
+
cred_path = ::File.join(credentials_dir, 'credentials')
|
|
165
|
+
::File.write(cred_path, token_val)
|
|
166
|
+
::File.chmod(0o600, cred_path)
|
|
167
|
+
end
|
|
84
168
|
end
|
|
85
169
|
end
|
|
86
170
|
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'thor'
|
|
4
|
+
require 'legion/cli/output'
|
|
5
|
+
|
|
6
|
+
module Legion
|
|
7
|
+
module CLI
|
|
8
|
+
class Tty < Thor
|
|
9
|
+
def self.exit_on_failure?
|
|
10
|
+
true
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
class_option :no_color, type: :boolean, default: false, desc: 'Disable color output'
|
|
14
|
+
class_option :config_dir, type: :string, desc: 'Config directory (~/.legionio/settings)'
|
|
15
|
+
class_option :skip_rain, type: :boolean, default: false, desc: 'Skip the digital rain intro'
|
|
16
|
+
|
|
17
|
+
default_task :interactive
|
|
18
|
+
|
|
19
|
+
desc 'interactive', 'Launch the rich terminal UI (default)'
|
|
20
|
+
long_desc <<~DESC
|
|
21
|
+
Launches the Legion TTY - a rich terminal interface with:
|
|
22
|
+
- Onboarding wizard (first run)
|
|
23
|
+
- AI chat shell with streaming responses
|
|
24
|
+
- Operational dashboard (Ctrl+D or /dashboard)
|
|
25
|
+
- Session persistence across runs
|
|
26
|
+
|
|
27
|
+
Similar to tools like Claude Code (CLI) and OpenAI Codex,
|
|
28
|
+
but purpose-built for LegionIO's async cognition engine.
|
|
29
|
+
|
|
30
|
+
First run: walks you through identity detection (Kerberos/GitHub),
|
|
31
|
+
provider selection, and API key setup.
|
|
32
|
+
|
|
33
|
+
Subsequent runs: loads saved identity, re-scans environment,
|
|
34
|
+
and drops straight into the chat shell.
|
|
35
|
+
DESC
|
|
36
|
+
def interactive
|
|
37
|
+
require_tty_gem
|
|
38
|
+
config_dir = options[:config_dir] || Legion::TTY::App::CONFIG_DIR
|
|
39
|
+
app = Legion::TTY::App.new(config_dir: config_dir)
|
|
40
|
+
app.start
|
|
41
|
+
rescue Interrupt
|
|
42
|
+
app&.shutdown
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
desc 'reset', 'Clear saved identity and credentials (re-run onboarding)'
|
|
46
|
+
option :confirm, type: :boolean, default: false, aliases: ['-y'], desc: 'Skip confirmation'
|
|
47
|
+
def reset
|
|
48
|
+
out = formatter
|
|
49
|
+
config_dir = options[:config_dir] || File.expand_path('~/.legionio/settings')
|
|
50
|
+
|
|
51
|
+
identity = File.join(config_dir, 'identity.json')
|
|
52
|
+
credentials = File.join(config_dir, 'credentials.json')
|
|
53
|
+
|
|
54
|
+
unless options[:confirm]
|
|
55
|
+
out.warn('This will delete your saved identity and credentials.')
|
|
56
|
+
out.warn('You will need to re-run onboarding.')
|
|
57
|
+
require 'tty-prompt'
|
|
58
|
+
prompt = ::TTY::Prompt.new
|
|
59
|
+
return unless prompt.yes?('Continue?')
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
[identity, credentials].each do |path|
|
|
63
|
+
if File.exist?(path)
|
|
64
|
+
File.delete(path)
|
|
65
|
+
out.success("Deleted #{File.basename(path)}")
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
desc 'sessions', 'List saved chat sessions'
|
|
71
|
+
def sessions
|
|
72
|
+
out = formatter
|
|
73
|
+
require_tty_gem
|
|
74
|
+
|
|
75
|
+
store = Legion::TTY::SessionStore.new
|
|
76
|
+
list = store.list
|
|
77
|
+
|
|
78
|
+
if list.empty?
|
|
79
|
+
out.detail('No saved sessions.')
|
|
80
|
+
return
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
list.each do |session|
|
|
84
|
+
name = session[:name]
|
|
85
|
+
count = session[:message_count]
|
|
86
|
+
saved = session[:saved_at] || 'unknown'
|
|
87
|
+
puts " #{name.ljust(30)} #{count} messages #{saved}"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
desc 'version', 'Show legion-tty version'
|
|
92
|
+
def version
|
|
93
|
+
require_tty_gem
|
|
94
|
+
puts "legion-tty #{Legion::TTY::VERSION}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
no_commands do
|
|
98
|
+
def formatter
|
|
99
|
+
@formatter ||= Output::Formatter.new(
|
|
100
|
+
json: false,
|
|
101
|
+
color: !options[:no_color]
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
def require_tty_gem
|
|
108
|
+
require 'legion/tty'
|
|
109
|
+
rescue LoadError => e
|
|
110
|
+
formatter.error("legion-tty gem not installed: #{e.message}")
|
|
111
|
+
formatter.detail('Install with: gem install legion-tty')
|
|
112
|
+
raise SystemExit, 1
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
data/lib/legion/cli.rb
CHANGED
|
@@ -41,6 +41,7 @@ module Legion
|
|
|
41
41
|
autoload :Cost, 'legion/cli/cost_command'
|
|
42
42
|
autoload :Marketplace, 'legion/cli/marketplace_command'
|
|
43
43
|
autoload :Notebook, 'legion/cli/notebook_command'
|
|
44
|
+
autoload :Tty, 'legion/cli/tty_command'
|
|
44
45
|
|
|
45
46
|
class Main < Thor
|
|
46
47
|
def self.exit_on_failure?
|
|
@@ -227,6 +228,9 @@ module Legion
|
|
|
227
228
|
desc 'notebook', 'Read and export Jupyter notebooks'
|
|
228
229
|
subcommand 'notebook', Legion::CLI::Notebook
|
|
229
230
|
|
|
231
|
+
desc 'tty', 'Rich terminal UI (onboarding, AI chat, dashboard)'
|
|
232
|
+
subcommand 'tty', Legion::CLI::Tty
|
|
233
|
+
|
|
230
234
|
desc 'tree', 'Print a tree of all available commands'
|
|
231
235
|
def tree
|
|
232
236
|
legion_print_command_tree(self.class, 'legion', '')
|
data/lib/legion/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: legionio
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.4.
|
|
4
|
+
version: 1.4.60
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -319,6 +319,7 @@ files:
|
|
|
319
319
|
- lib/legion/api/audit.rb
|
|
320
320
|
- lib/legion/api/auth.rb
|
|
321
321
|
- lib/legion/api/auth_human.rb
|
|
322
|
+
- lib/legion/api/auth_kerberos.rb
|
|
322
323
|
- lib/legion/api/auth_worker.rb
|
|
323
324
|
- lib/legion/api/capacity.rb
|
|
324
325
|
- lib/legion/api/chains.rb
|
|
@@ -479,6 +480,7 @@ files:
|
|
|
479
480
|
- lib/legion/cli/theme.rb
|
|
480
481
|
- lib/legion/cli/trace_command.rb
|
|
481
482
|
- lib/legion/cli/trigger.rb
|
|
483
|
+
- lib/legion/cli/tty_command.rb
|
|
482
484
|
- lib/legion/cli/update_command.rb
|
|
483
485
|
- lib/legion/cli/version.rb
|
|
484
486
|
- lib/legion/cli/worker_command.rb
|