lex-github 0.3.1 → 0.3.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eebe64eb1c8b63779a68d69cea578a978c3051d54cb7e446c53778d0d9af27c0
4
- data.tar.gz: d7b0a6c2f444212f4e36ec4cb96394db42a64beaaf8a8597094e079f75f62454
3
+ metadata.gz: 375eae72829e4b18ecd5162a92eecb0a93f22552e51bdedee79866d46d720fdd
4
+ data.tar.gz: 543b65111d082eec0abfa3a2e1c9d44e7e248d5ded04a6664f26f1f5d1025b00
5
5
  SHA512:
6
- metadata.gz: 8a9f9e5af627a04456364243839cb8984eefaa5576a5dfcee75284fb40c5fbac6fb12f69334c1f8b36f00b1538e57a484f057255d506f1a3a249de94c10d4d7d
7
- data.tar.gz: 95d3863eb968f7f26d415e0d56bfff81e3e57a911183abd95f5734ba0a45dfc6201fa8bf92227d030752be4f225590e824ae9e15148cf5adcbdbdb0275b7b5a7
6
+ metadata.gz: 7d3f74a3a0bd859ca3c8a35afca74a6c45afb7d057f2aff06fa2f6842b77a94ca5afaa840c08d323a1172f1385d3db6f9f34fb5b886a6deae5cfe98f5b45f139
7
+ data.tar.gz: 4c19e0bbae310e0987bbda49565a525c5cb6bf90abb4a91c81134834d4a53f671160e4767b6ec20862a68f8d680ddf4083be635e079752d7e217a5f5b353db34
data/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.3.3] - 2026-03-31
6
+
7
+ ### Fixed
8
+ - CLI runner output: `status` and `login` commands now print JSON results to stdout
9
+ - CLI runner errors print to stderr via `warn`
10
+
11
+ ## [0.3.2] - 2026-03-31
12
+
13
+ ### Added
14
+ - CLI command registration: `legionio lex exec github auth status|login` and `legionio lex exec github app setup|complete_setup`
15
+ - `CLI::AuthRunner` and `CLI::AppRunner` wrapper classes for `lex exec` dispatch
16
+ - Self-registering CLI manifest at `~/.legionio/cache/cli/lex-github.json` (written on first require)
17
+ - Require redirect `lib/lex/github.rb` for `lex exec` compatibility
18
+
5
19
  ## [0.3.1] - 2026-03-30
6
20
 
7
21
  ### Changed
@@ -15,9 +15,9 @@ module Legion
15
15
  csec = client_secret || settings_client_secret
16
16
  sc = scopes || settings_scopes
17
17
 
18
- unless cid && csec
18
+ unless cid
19
19
  return { error: 'missing_config',
20
- description: 'Set github.oauth.client_id or github.app.client_id and github.app.client_secret in settings or pass as arguments' }
20
+ description: 'Set github.oauth.client_id or github.app.client_id in settings' }
21
21
  end
22
22
 
23
23
  browser = Helpers::BrowserAuth.new(client_id: cid, client_secret: csec, scopes: sc)
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'net/http'
5
+ require 'uri'
6
+ require 'rbconfig'
7
+
8
+ module Legion
9
+ module Extensions
10
+ module Github
11
+ module CLI
12
+ DAEMON_URL = ENV.fetch('LEGION_API_URL', 'http://127.0.0.1:4567')
13
+
14
+ module DaemonApi
15
+ private
16
+
17
+ def api_post(path, body = {})
18
+ uri = URI("#{DAEMON_URL}#{path}")
19
+ http = Net::HTTP.new(uri.host, uri.port)
20
+ http.open_timeout = 5
21
+ http.read_timeout = 30
22
+ request = Net::HTTP::Post.new(uri.path, 'Content-Type' => 'application/json')
23
+ request.body = ::JSON.generate(body)
24
+ parse_response(http.request(request))
25
+ rescue Errno::ECONNREFUSED, Errno::ECONNRESET => _e
26
+ { error: 'daemon_unavailable', description: "Legion daemon not running at #{DAEMON_URL}. Start it with: legionio start" }
27
+ end
28
+
29
+ def api_get(path)
30
+ uri = URI("#{DAEMON_URL}#{path}")
31
+ parse_response(Net::HTTP.get_response(uri))
32
+ rescue Errno::ECONNREFUSED, Errno::ECONNRESET => _e
33
+ { error: 'daemon_unavailable', description: "Legion daemon not running at #{DAEMON_URL}. Start it with: legionio start" }
34
+ end
35
+
36
+ def parse_response(response)
37
+ ::JSON.parse(response.body, symbolize_names: true)
38
+ rescue ::JSON::ParserError => _e
39
+ { error: "http_#{response.code}", description: response.body&.strip }
40
+ end
41
+
42
+ def print_json(result)
43
+ if result.is_a?(Hash) && result[:error]
44
+ warn "Error: #{result[:error]}"
45
+ warn " #{result[:description]}" if result[:description]
46
+ else
47
+ puts ::JSON.pretty_generate(result)
48
+ end
49
+ end
50
+
51
+ def open_browser(url)
52
+ cmd = case RbConfig::CONFIG['host_os']
53
+ when /darwin/ then 'open'
54
+ when /linux/ then 'xdg-open'
55
+ when /mswin|mingw/ then 'start'
56
+ end
57
+ system(cmd, url) if cmd
58
+ end
59
+ end
60
+
61
+ class AuthRunner
62
+ include DaemonApi
63
+
64
+ def status
65
+ print_json(api_post('/api/extensions/github/runners/auth/status'))
66
+ end
67
+
68
+ def login
69
+ print_json(api_post('/api/extensions/github/runners/auth/login'))
70
+ end
71
+ end
72
+
73
+ class AppRunner
74
+ include DaemonApi
75
+
76
+ def setup
77
+ result = api_post('/api/extensions/github/cli/app/setup')
78
+
79
+ if result[:error]
80
+ print_json(result)
81
+ return
82
+ end
83
+
84
+ url = result.dig(:data, :manifest_url)
85
+ if url
86
+ warn 'Opening browser to create GitHub App...'
87
+ open_browser(url)
88
+ warn 'Waiting for callback...'
89
+ poll = api_post('/api/extensions/github/cli/app/await_callback',
90
+ { timeout: 300 })
91
+ print_json(poll)
92
+ else
93
+ print_json(result)
94
+ end
95
+ end
96
+
97
+ def complete_setup
98
+ print_json(api_post('/api/extensions/github/cli/app/complete_setup'))
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -14,7 +14,7 @@ module Legion
14
14
 
15
15
  attr_reader :client_id, :client_secret, :scopes
16
16
 
17
- def initialize(client_id:, client_secret:, scopes: DEFAULT_SCOPES, auth: nil, **)
17
+ def initialize(client_id:, client_secret: nil, scopes: DEFAULT_SCOPES, auth: nil, **)
18
18
  @client_id = client_id
19
19
  @client_secret = client_secret
20
20
  @scopes = scopes
@@ -33,21 +33,19 @@ module Legion
33
33
  { result: "https://github.com/login/oauth/authorize?#{params}" }
34
34
  end
35
35
 
36
- def exchange_code(client_id:, client_secret:, code:, redirect_uri:, code_verifier:, **)
37
- response = oauth_connection.post('/login/oauth/access_token', {
38
- client_id: client_id, client_secret: client_secret,
39
- code: code, redirect_uri: redirect_uri,
40
- code_verifier: code_verifier
41
- })
36
+ def exchange_code(client_id:, code:, redirect_uri:, code_verifier:, client_secret: nil, **)
37
+ body = { client_id: client_id, code: code,
38
+ redirect_uri: redirect_uri, code_verifier: code_verifier }
39
+ body[:client_secret] = client_secret if client_secret
40
+ response = oauth_connection.post('/login/oauth/access_token', body)
42
41
  { result: response.body }
43
42
  end
44
43
 
45
- def refresh_token(client_id:, client_secret:, refresh_token:, **)
46
- response = oauth_connection.post('/login/oauth/access_token', {
47
- client_id: client_id, client_secret: client_secret,
48
- refresh_token: refresh_token,
49
- grant_type: 'refresh_token'
50
- })
44
+ def refresh_token(client_id:, refresh_token:, client_secret: nil, **)
45
+ body = { client_id: client_id, refresh_token: refresh_token,
46
+ grant_type: 'refresh_token' }
47
+ body[:client_secret] = client_secret if client_secret
48
+ response = oauth_connection.post('/login/oauth/access_token', body)
51
49
  { result: response.body }
52
50
  end
53
51
 
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/github/helpers/client'
4
+ require 'legion/extensions/github/helpers/browser_auth'
5
+ require 'legion/extensions/github/app/runners/auth'
6
+ require 'legion/extensions/github/app/runners/credential_store'
7
+ require 'legion/extensions/github/oauth/runners/auth'
8
+
9
+ module Legion
10
+ module Extensions
11
+ module Github
12
+ module Runners
13
+ module Auth
14
+ include Legion::Extensions::Github::Helpers::Client
15
+ include Legion::Extensions::Github::App::Runners::Auth
16
+ include Legion::Extensions::Github::App::Runners::CredentialStore
17
+ include Legion::Extensions::Github::OAuth::Runners::Auth
18
+
19
+ def self.remote_invocable?
20
+ false
21
+ end
22
+
23
+ def status(**)
24
+ cred = resolve_credential
25
+ unless cred
26
+ log.warn('[lex-github] auth status: no credential found across all sources')
27
+ return { result: { authenticated: false } }
28
+ end
29
+
30
+ log.info("[lex-github] auth status: credential found via #{cred[:auth_type]}")
31
+
32
+ user_info = {}
33
+ scopes = nil
34
+ begin
35
+ response = connection(token: cred[:token]).get('/user')
36
+ user_info = response.body || {}
37
+ headers = response.respond_to?(:headers) ? response.headers : {}
38
+ scopes_header = headers['X-OAuth-Scopes'] || headers['x-oauth-scopes']
39
+ scopes = scopes_header&.split(',')&.map(&:strip)
40
+ log.info("[lex-github] auth status: authenticated as #{user_info['login']} (#{cred[:auth_type]})")
41
+ rescue StandardError => e
42
+ log.warn("[lex-github] auth status: credential found but /user request failed: #{e.message}")
43
+ end
44
+
45
+ { result: { authenticated: true, auth_type: cred[:auth_type],
46
+ user: user_info['login'], scopes: scopes } }
47
+ end
48
+
49
+ def login(client_id: nil, scopes: nil, **)
50
+ cid = client_id || settings_client_id
51
+ unless cid
52
+ log.error('[lex-github] auth login: no client_id configured — set github.app.client_id in settings')
53
+ return { error: 'missing_config', description: 'Set github.app.client_id in settings' }
54
+ end
55
+
56
+ log.info("[lex-github] auth login: starting OAuth flow with client_id=#{cid[0..7]}...")
57
+
58
+ sc = scopes || settings_scopes
59
+ browser = Helpers::BrowserAuth.new(client_id: cid, scopes: sc)
60
+ result = browser.authenticate
61
+
62
+ if result[:error]
63
+ log.error("[lex-github] auth login failed: #{result[:error]} — #{result[:description]}")
64
+ return { result: nil, error: result[:error], description: result[:description] }
65
+ end
66
+
67
+ if result[:result]&.dig('access_token')
68
+ user = begin
69
+ current_user(token: result[:result]['access_token'])
70
+ rescue StandardError => e
71
+ log.warn("[lex-github] auth login: token obtained but /user lookup failed: #{e.message}")
72
+ 'default'
73
+ end
74
+
75
+ log.info("[lex-github] auth login: authenticated as #{user}")
76
+
77
+ if respond_to?(:store_oauth_token, true)
78
+ store_oauth_token(
79
+ user: user,
80
+ access_token: result[:result]['access_token'],
81
+ refresh_token: result[:result]['refresh_token'],
82
+ expires_in: result[:result]['expires_in']
83
+ )
84
+ log.info("[lex-github] auth login: token stored for user=#{user}")
85
+ else
86
+ log.warn('[lex-github] auth login: store_oauth_token not available — token not persisted')
87
+ end
88
+ else
89
+ log.warn('[lex-github] auth login: OAuth completed but no access_token in response')
90
+ end
91
+
92
+ result
93
+ end
94
+
95
+ def installations(**)
96
+ log.info('[lex-github] listing app installations')
97
+ list_installations(**)
98
+ end
99
+
100
+ private
101
+
102
+ def current_user(token:)
103
+ connection(token: token).get('/user').body['login']
104
+ end
105
+
106
+ def settings_client_id
107
+ defined?(Legion::Settings) &&
108
+ (Legion::Settings.dig(:github, :oauth, :client_id) ||
109
+ Legion::Settings.dig(:github, :app, :client_id))
110
+ rescue StandardError => _e
111
+ nil
112
+ end
113
+
114
+ def settings_scopes
115
+ defined?(Legion::Settings) && Legion::Settings.dig(:github, :oauth, :scopes)
116
+ rescue StandardError => _e
117
+ nil
118
+ end
119
+
120
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
121
+ Legion::Extensions::Helpers.const_defined?(:Lex, false)
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module Github
6
- VERSION = '0.3.1'
6
+ VERSION = '0.3.3'
7
7
  end
8
8
  end
9
9
  end
@@ -35,13 +35,51 @@ require 'legion/extensions/github/runners/actions'
35
35
  require 'legion/extensions/github/runners/checks'
36
36
  require 'legion/extensions/github/runners/releases'
37
37
  require 'legion/extensions/github/runners/deployments'
38
+ require 'legion/extensions/github/runners/auth'
38
39
  require 'legion/extensions/github/runners/repository_webhooks'
39
40
  require 'legion/extensions/github/client'
41
+ require 'legion/extensions/github/cli/runner'
40
42
 
41
43
  module Legion
42
44
  module Extensions
43
45
  module Github
44
46
  extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core, false
47
+
48
+ CLI_COMMANDS = {
49
+ 'auth' => {
50
+ class_name: 'Legion::Extensions::Github::CLI::AuthRunner',
51
+ methods: {
52
+ 'login' => { desc: 'Authenticate with GitHub via OAuth browser flow', args: '' },
53
+ 'status' => { desc: 'Show current GitHub authentication status', args: '' }
54
+ }
55
+ },
56
+ 'app' => {
57
+ class_name: 'Legion::Extensions::Github::CLI::AppRunner',
58
+ methods: {
59
+ 'setup' => { desc: 'Create a new GitHub App via manifest flow', args: '' },
60
+ 'complete_setup' => { desc: 'Complete GitHub App setup with authorization code', args: '' }
61
+ }
62
+ }
63
+ }.freeze
64
+
65
+ begin
66
+ manifest_dir = ::File.expand_path('~/.legionio/cache/cli')
67
+ manifest_path = ::File.join(manifest_dir, 'lex-github.json')
68
+ unless ::File.exist?(manifest_path) && ::File.read(manifest_path).include?(VERSION)
69
+ require 'fileutils'
70
+ ::FileUtils.mkdir_p(manifest_dir)
71
+ serialized = CLI_COMMANDS.transform_values do |cmd|
72
+ { 'class' => cmd[:class_name],
73
+ 'methods' => cmd[:methods].transform_values { |m| { 'desc' => m[:desc], 'args' => m[:args] } } }
74
+ end
75
+ ::File.write(manifest_path, ::JSON.pretty_generate(
76
+ 'gem' => 'lex-github', 'version' => VERSION,
77
+ 'alias' => 'github', 'commands' => serialized
78
+ ))
79
+ end
80
+ rescue StandardError => e
81
+ warn "[lex-github] CLI manifest write skipped: #{e.message}" if ENV['LEGION_DEBUG']
82
+ end
45
83
  end
46
84
  end
47
85
  end
data/lib/lex/github.rb ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Redirect for `require 'lex/github'` (used by `legionio lex exec`)
4
+ require 'legion/extensions/github'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-github
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -185,6 +185,7 @@ files:
185
185
  - lib/legion/extensions/github/app/transport/queues/webhooks.rb
186
186
  - lib/legion/extensions/github/cli/app.rb
187
187
  - lib/legion/extensions/github/cli/auth.rb
188
+ - lib/legion/extensions/github/cli/runner.rb
188
189
  - lib/legion/extensions/github/client.rb
189
190
  - lib/legion/extensions/github/errors.rb
190
191
  - lib/legion/extensions/github/helpers/browser_auth.rb
@@ -202,6 +203,7 @@ files:
202
203
  - lib/legion/extensions/github/oauth/transport/exchanges/oauth.rb
203
204
  - lib/legion/extensions/github/oauth/transport/queues/auth.rb
204
205
  - lib/legion/extensions/github/runners/actions.rb
206
+ - lib/legion/extensions/github/runners/auth.rb
205
207
  - lib/legion/extensions/github/runners/branches.rb
206
208
  - lib/legion/extensions/github/runners/checks.rb
207
209
  - lib/legion/extensions/github/runners/comments.rb
@@ -219,6 +221,7 @@ files:
219
221
  - lib/legion/extensions/github/runners/search.rb
220
222
  - lib/legion/extensions/github/runners/users.rb
221
223
  - lib/legion/extensions/github/version.rb
224
+ - lib/lex/github.rb
222
225
  homepage: https://github.com/LegionIO/lex-github
223
226
  licenses:
224
227
  - MIT