truenorth 0.3.0 → 0.5.0

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: 8d89d28373a48164f8f6cf1f93672dd70d4f8aaf8035ae40bdc144fe76a8d6be
4
- data.tar.gz: 266756fa35b43d17ad657bb11ab46d0411c63da6620bf630594a08ac7f4d5226
3
+ metadata.gz: 1ae8cfc6540c62e513428be9bf6b460cba5f1207f405d5dd075d2f3754fe9976
4
+ data.tar.gz: 20382b0505099606ebf214359796a956947ee6cf3667b7ac8d5797debf7e995b
5
5
  SHA512:
6
- metadata.gz: e1c0d586aa1b9146bedb498c81036d1446f23a9f6ee92cae1d14887007227196986e4ed245511cd39a58375729fa55831174f1536721f5d57a39ca8db03983bb
7
- data.tar.gz: a764f05fe8325d6457f03467ba1c3a169b9422b8bbaa8d2030264a0f8bf5bcacda0e346fd913c0d415880f26d1f6dde03c6ef3fd58525c80e8e5ef2153b862c3
6
+ metadata.gz: 38de51f859b0c3c6f20a2c23d2bffabc91de6de3d4e8f065a9e9f25bacfa2b1ac6d53d5aed5e8755a67ba8b99ed46909a63f8ea891929958e0e0e020bdb37949
7
+ data.tar.gz: e8372584aaa758b89b29c620e0c31ff63d97740e1a8bfd3ccaec52e88a063b2934e05aad1c465995454d0ffa1171596d4fd74d3dc26160c85f1e5b2aa1a3249c
data/lib/truenorth/cli.rb CHANGED
@@ -35,9 +35,17 @@ module Truenorth
35
35
  desc 'availability [DATE]', 'Check available slots'
36
36
  option :activity, aliases: '-a', default: 'squash', desc: 'Activity type (squash, golf, music, meeting)'
37
37
  option :json, type: :boolean, desc: 'Output as JSON'
38
+ option :debug, type: :boolean, desc: 'Show debug output'
39
+ option :http, type: :boolean, desc: 'Use HTTP mode (faster, but only 2 courts)'
38
40
  def availability(date = nil)
39
41
  date = parse_date(date)
40
- client = Client.new
42
+
43
+ client = if options[:http]
44
+ say 'Using HTTP mode (fast, 2 courts only)...', :yellow if !options[:json]
45
+ Client.new(debug: options[:debug])
46
+ else
47
+ BrowserClient.new(debug: options[:debug])
48
+ end
41
49
 
42
50
  say "Checking availability for #{date}...", :cyan
43
51
  result = client.availability(date, activity: options[:activity])
@@ -64,6 +72,8 @@ module Truenorth
64
72
  option :court, aliases: '-c', desc: 'Preferred court (e.g., "Court 1", "Squash Court 2")'
65
73
  option :activity, aliases: '-a', default: 'squash', desc: 'Activity type'
66
74
  option :dry_run, type: :boolean, aliases: '-n', desc: 'Test without actually booking'
75
+ option :http, type: :boolean, desc: 'Use HTTP mode (faster, but only 2 courts)'
76
+ option :debug, type: :boolean, desc: 'Show debug output'
67
77
  def book(time_or_description)
68
78
  # Parse natural language input
69
79
  parsed = parse_booking_request(time_or_description, options[:date], options[:activity])
@@ -71,7 +81,13 @@ module Truenorth
71
81
  date = parsed[:date]
72
82
  time = parsed[:time]
73
83
  activity = parsed[:activity]
74
- client = Client.new
84
+
85
+ # Use browser mode unless --http flag is set (needed to see all 3 courts)
86
+ client = if options[:http]
87
+ Client.new(debug: options[:debug])
88
+ else
89
+ BrowserClient.new(debug: options[:debug])
90
+ end
75
91
 
76
92
  mode = options[:dry_run] ? ' (DRY RUN)' : ''
77
93
  say "Booking #{activity} at #{time} on #{date}#{mode}...", :cyan
@@ -109,8 +125,10 @@ module Truenorth
109
125
  desc 'reservations', 'List your current reservations'
110
126
  option :json, type: :boolean, desc: 'Output as JSON'
111
127
  option :all, type: :boolean, aliases: '-a', desc: 'Show all family members (default: only you)'
128
+ option :debug, type: :boolean, desc: 'Show debug output'
112
129
  def reservations
113
- client = Client.new
130
+ # Use HTTP mode for reservations (works reliably)
131
+ client = Client.new(debug: options[:debug])
114
132
 
115
133
  say 'Fetching reservations...', :cyan
116
134
  results = client.reservations
@@ -153,6 +171,7 @@ module Truenorth
153
171
  option :all, type: :boolean, aliases: '-a', desc: 'Cancel from full list (use with --all view)'
154
172
  option :debug, type: :boolean, desc: 'Show debug output'
155
173
  def cancel(index)
174
+ # Use HTTP mode for cancellation (works reliably)
156
175
  client = Client.new(debug: options[:debug])
157
176
 
158
177
  say 'Fetching reservations...', :cyan
@@ -15,7 +15,8 @@ module Truenorth
15
15
  BOOKING_PATH = '/group/pages/facility-booking'
16
16
  RESERVATIONS_PATH = '/group/pages/my-reservations'
17
17
 
18
- USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ' \
18
+ # Use a desktop user agent - server may detect mobile based on UA
19
+ USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ' \
19
20
  'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'
20
21
 
21
22
  # Activity type IDs
@@ -37,16 +38,27 @@ module Truenorth
37
38
  '32' => 'Court 3'
38
39
  }.freeze
39
40
 
41
+ # Court IDs by activity
42
+ COURT_IDS_BY_ACTIVITY = {
43
+ 'squash' => %w[16 17 18],
44
+ 'golf' => %w[30 31 32],
45
+ 'music' => %w[16 17 18], # May need to update these
46
+ 'room' => %w[30 31 32], # May need to update these
47
+ 'meeting' => %w[30 31 32] # May need to update these
48
+ }.freeze
49
+
40
50
  attr_reader :cookies, :debug_log, :logged_in
41
51
 
42
52
  def initialize(base_url: nil, debug: false)
43
53
  @base_url = base_url || Config.base_url
44
54
  raise Error, 'No base URL configured. Run: truenorth configure' unless @base_url
45
55
 
46
- @cookies = {}
56
+ @cookies = Config.cookies || {}
47
57
  @debug = debug
48
58
  @debug_log = StringIO.new
49
- @logged_in = false
59
+ @logged_in = !@cookies.empty? # If we have cookies, assume logged in
60
+
61
+ log "Loaded #{@cookies.length} cookies from cache" if @logged_in && @debug
50
62
  end
51
63
 
52
64
  # Login to the booking system
@@ -91,7 +103,8 @@ module Truenorth
91
103
 
92
104
  if response.body.include?('Sign Out') || response.body.include?('My Reservations')
93
105
  @logged_in = true
94
- log 'Login successful'
106
+ Config.save_cookies(@cookies)
107
+ log 'Login successful (cookies saved)'
95
108
  true
96
109
  else
97
110
  html = Nokogiri::HTML(response.body)
@@ -128,8 +141,52 @@ module Truenorth
128
141
  html = change_activity(html, activity_id)
129
142
  end
130
143
 
131
- slots = parse_slots(html)
132
- log "Found #{slots.count} available time slots"
144
+ # Debug: Save HTML after activity change
145
+ if @debug
146
+ debug_file = "/tmp/truenorth_after_activity_#{Time.now.to_i}.html"
147
+ File.write(debug_file, html.to_s)
148
+ log "Saved HTML after activity change to: #{debug_file}"
149
+ end
150
+
151
+ # Court dropdown not available in HTML response - use hardcoded IDs
152
+ court_ids = COURT_IDS_BY_ACTIVITY[activity.to_s.downcase] || []
153
+
154
+ if court_ids.length >= 3
155
+ log "Querying #{court_ids.length} courts individually (IDs: #{court_ids.join(', ')})"
156
+ all_slots = {}
157
+
158
+ court_ids.each do |court_id|
159
+ court_name = COURTS[court_id] || "Court #{court_id}"
160
+ log "Querying #{court_name} (ID: #{court_id})"
161
+
162
+ # Set activityAreaId and refresh
163
+ form_id = extract_form_id(html)
164
+ view_state = extract_view_state(html)
165
+ form_fields = extract_all_form_fields(html, form_id)
166
+ form_fields["#{form_id}:activityAreaId"] = court_id
167
+
168
+ # Make AJAX request to update table for this court
169
+ result = change_date_ajax(form_id, view_state, requested_date, form_fields)
170
+ if result[:success]
171
+ court_html = Nokogiri::HTML(result[:body].scan(/<!\[CDATA\[(.*?)\]\]>/m).flatten.join("\n"))
172
+ court_slots = parse_slots(court_html)
173
+
174
+ # Merge slots
175
+ court_slots.each do |time, courts|
176
+ all_slots[time] ||= []
177
+ all_slots[time].concat(courts)
178
+ all_slots[time].uniq!
179
+ end
180
+ end
181
+ end
182
+
183
+ slots = all_slots
184
+ log "Combined #{slots.count} time slots from all courts"
185
+ else
186
+ # Fallback to original parsing
187
+ slots = parse_slots(html)
188
+ log "Found #{slots.count} available time slots"
189
+ end
133
190
 
134
191
  {
135
192
  success: true,
@@ -141,7 +198,8 @@ module Truenorth
141
198
 
142
199
 
143
200
  # Book a slot
144
- def book(time, date: Date.today, court: nil, activity: 'squash', dry_run: false)
201
+ # If slot_info is provided, it should have: { area_id:, start_time:, end_time:, court: }
202
+ def book(time, date: Date.today, court: nil, activity: 'squash', dry_run: false, slot_info: nil)
145
203
  ensure_logged_in!
146
204
 
147
205
  log "\n=== BOOK SLOT ==="
@@ -172,11 +230,22 @@ module Truenorth
172
230
  form_fields = extract_all_form_fields(html, form_id)
173
231
  end
174
232
 
175
- # Find the slot
176
- slot = find_slot(html, time, court)
177
- raise BookingError, "No slot available at #{time}" unless slot
178
-
179
- log "Found slot: #{slot[:court]} at #{slot[:start_time]}"
233
+ # Find the slot (or use provided slot_info)
234
+ if slot_info
235
+ log "Using provided slot info: #{slot_info[:court]} at #{slot_info[:start_time]}"
236
+ # Convert slot_info keys from symbols if needed and ensure we have an id
237
+ slot = {
238
+ id: nil, # We'll generate this or it's not needed for AJAX
239
+ area_id: slot_info[:area_id],
240
+ court: slot_info[:court],
241
+ start_time: slot_info[:start_time] || slot_info[:time],
242
+ end_time: slot_info[:end_time]
243
+ }
244
+ else
245
+ slot = find_slot(html, time, court)
246
+ raise BookingError, "No slot available at #{time}" unless slot
247
+ log "Found slot: #{slot[:court]} at #{slot[:start_time]}"
248
+ end
180
249
 
181
250
  # Select slot via AJAX
182
251
  select_result = select_slot_ajax(form_id, view_state, slot, components, form_fields)
@@ -417,6 +486,18 @@ module Truenorth
417
486
  def parse_slots(html)
418
487
  slots = {}
419
488
 
489
+ # Debug: Count total columns found
490
+ headers = html.css('thead th[role="columnheader"]')
491
+ log "Found #{headers.length} column headers: #{headers.map { |h| h['aria-label'] }.join(', ')}"
492
+
493
+ # Debug: Count slots by area-id
494
+ area_counts = Hash.new(0)
495
+ html.css('td.slot div[data-start-time]').each do |div|
496
+ area_id = div['data-area-id']
497
+ area_counts[area_id] += 1
498
+ end
499
+ log "Slots by area ID: #{area_counts.inspect}"
500
+
420
501
  # Find ALL slots with data-start-time
421
502
  html.css('td.slot div[data-start-time]').each do |div|
422
503
  td = div.parent
@@ -590,9 +671,66 @@ module Truenorth
590
671
  result = change_activity_ajax(form_id, view_state, activity_id, form_fields, components)
591
672
  return html unless result[:success]
592
673
 
674
+ # Parse the response and check for Court 3
675
+ parsed = Nokogiri::HTML(result[:body])
676
+ court3_elements = parsed.css('th[aria-label*="Court 3"], div[data-area-id="18"], div[data-area-id="32"]')
677
+ log "After change_activity: Found #{court3_elements.length} Court 3 elements in response"
678
+
679
+ parsed
680
+ end
681
+
682
+ def change_court_dropdown(html, court_id)
683
+ form_id = extract_form_id(html)
684
+ view_state = extract_view_state(html)
685
+ form_fields = extract_all_form_fields(html, form_id)
686
+
687
+ # Find the court dropdown field dynamically (in .activity-areas div)
688
+ court_dropdown = html.at_css('.activity-areas select, div[class*="area"] select[id*="j_idt"]')
689
+ return html unless court_dropdown
690
+
691
+ dropdown_id = court_dropdown['id']
692
+ dropdown_source = dropdown_id.gsub(/_input$/, '')
693
+
694
+ result = change_court_dropdown_ajax(form_id, view_state, court_id, dropdown_id, dropdown_source, form_fields)
695
+ return html unless result[:success]
696
+
697
+ # Parse CDATA content from response
698
+ cdata_content = result[:body].scan(/<!\[CDATA\[(.*?)\]\]>/m).flatten.join("\n")
699
+ if !cdata_content.empty?
700
+ cdata_html = Nokogiri::HTML(cdata_content)
701
+ slots = cdata_html.css('div[data-start-time]')
702
+ log "Court #{court_id}: Found #{slots.length} slots in CDATA response"
703
+ return cdata_html if slots.length > 0
704
+ end
705
+
593
706
  Nokogiri::HTML(result[:body])
594
707
  end
595
708
 
709
+ def change_court_area(html, area_id)
710
+ form_id = extract_form_id(html)
711
+ view_state = extract_view_state(html)
712
+ form_fields = extract_all_form_fields(html, form_id)
713
+
714
+ result = change_court_area_ajax(form_id, view_state, area_id, form_fields)
715
+ return html unless result[:success]
716
+
717
+ # Debug: Check response
718
+ parsed = Nokogiri::HTML(result[:body])
719
+ slot_divs = parsed.css('div[data-start-time]')
720
+ log "Court area #{area_id} response: #{slot_divs.length} slot divs found"
721
+
722
+ # Also check CDATA content
723
+ cdata_content = result[:body].scan(/<!\[CDATA\[(.*?)\]\]>/m).flatten.join("\n")
724
+ if !cdata_content.empty?
725
+ cdata_html = Nokogiri::HTML(cdata_content)
726
+ cdata_slots = cdata_html.css('div[data-start-time]')
727
+ log "Court area #{area_id} CDATA: #{cdata_slots.length} slot divs in CDATA"
728
+ return cdata_html if cdata_slots.length > 0
729
+ end
730
+
731
+ parsed
732
+ end
733
+
596
734
  def change_date(html, date_str, activity_id = nil)
597
735
  # Extract what we can from the HTML
598
736
  form_id = extract_form_id(html)
@@ -609,7 +747,17 @@ module Truenorth
609
747
  cdata_content = ajax_body.scan(/<!\[CDATA\[(.*?)\]\]>/m).flatten.join("\n")
610
748
 
611
749
  if !cdata_content.empty? && cdata_content.include?('slot')
612
- Nokogiri::HTML(cdata_content)
750
+ parsed_html = Nokogiri::HTML(cdata_content)
751
+
752
+ # Debug: Check if Court 3 is in the response
753
+ court3_headers = parsed_html.css('th[aria-label*="Court 3"]')
754
+ log "Court 3 headers found in AJAX response: #{court3_headers.length}"
755
+
756
+ # Debug: Check for area ID 18 or 32 (Court 3 IDs)
757
+ court3_slots = parsed_html.css('div[data-area-id="18"], div[data-area-id="32"]')
758
+ log "Court 3 slots (area 18/32) found in AJAX response: #{court3_slots.length}"
759
+
760
+ parsed_html
613
761
  else
614
762
  html
615
763
  end
@@ -681,7 +829,22 @@ module Truenorth
681
829
 
682
830
  selected = select.at_css('option[selected]')
683
831
  fields[select['name']] = selected['value'] if selected
832
+
833
+ # Debug: Log select options
834
+ if select['name']&.include?('area') || select['name']&.include?('court') || select['name']&.include?('trainer')
835
+ options = select.css('option').map { |opt| "#{opt.text.strip}=#{opt['value']}" }
836
+ log "Found selector #{select['name']}: #{options.join(', ')}"
837
+ end
684
838
  end
839
+
840
+ # Force parameters to get all courts
841
+ fields["#{form_id}:mobileViewDisplay"] = '0' # 0 = full view
842
+
843
+ # Debug: Log all field names and key values
844
+ log "Form fields: #{fields.keys.join(', ')}"
845
+ log "activityAreaId current value: #{fields["#{form_id}:activityAreaId"]}"
846
+ log "showAllAreasOrTrainers: #{fields["#{form_id}:showAllAreasOrTrainers"]}"
847
+
685
848
  fields
686
849
  end
687
850
 
@@ -710,6 +873,12 @@ module Truenorth
710
873
  form_data = form_fields.dup
711
874
  form_data["#{form_id}:j_idt51_input"] = activity_id
712
875
  form_data["#{form_id}:activityId"] = activity_id
876
+ form_data["#{form_id}:showAllAreasOrTrainers"] = 'true' # Show all courts!
877
+
878
+ # Debug: Log what we're sending
879
+ log "Sending to #{form_id}:showAllAreasOrTrainers = #{form_data["#{form_id}:showAllAreasOrTrainers"]}"
880
+ log "Sending to #{form_id}:mobileViewDisplay = #{form_data["#{form_id}:mobileViewDisplay"]}"
881
+
713
882
  form_data.merge!(
714
883
  'javax.faces.partial.ajax' => 'true',
715
884
  'javax.faces.source' => activity_dropdown,
@@ -730,6 +899,56 @@ module Truenorth
730
899
  end
731
900
  end
732
901
 
902
+ def change_court_dropdown_ajax(form_id, view_state, court_id, dropdown_input_id, dropdown_source, form_fields)
903
+ ajax_url = build_ajax_url
904
+ encoded_url = URI.encode_www_form_component(ajax_url)
905
+
906
+ form_data = form_fields.dup
907
+ form_data[dropdown_input_id] = court_id
908
+ form_data.merge!(
909
+ 'javax.faces.partial.ajax' => 'true',
910
+ 'javax.faces.source' => dropdown_source,
911
+ 'javax.faces.partial.execute' => dropdown_source,
912
+ 'javax.faces.partial.render' => form_id,
913
+ 'javax.faces.behavior.event' => 'change',
914
+ 'javax.faces.partial.event' => 'change',
915
+ form_id => form_id,
916
+ 'javax.faces.encodedURL' => encoded_url,
917
+ 'javax.faces.ViewState' => view_state
918
+ )
919
+
920
+ response = post_ajax(ajax_url, form_data)
921
+ if response.is_a?(Net::HTTPSuccess)
922
+ { success: true, view_state: extract_view_state_from_ajax(response.body), body: response.body }
923
+ else
924
+ { success: false, error: "HTTP #{response.code}" }
925
+ end
926
+ end
927
+
928
+ def change_court_area_ajax(form_id, view_state, area_id, form_fields)
929
+ ajax_url = build_ajax_url
930
+ encoded_url = URI.encode_www_form_component(ajax_url)
931
+
932
+ form_data = form_fields.dup
933
+ form_data["#{form_id}:activityAreaId"] = area_id
934
+ form_data.merge!(
935
+ 'javax.faces.partial.ajax' => 'true',
936
+ 'javax.faces.source' => form_id,
937
+ 'javax.faces.partial.execute' => form_id,
938
+ 'javax.faces.partial.render' => form_id,
939
+ form_id => form_id,
940
+ 'javax.faces.encodedURL' => encoded_url,
941
+ 'javax.faces.ViewState' => view_state
942
+ )
943
+
944
+ response = post_ajax(ajax_url, form_data)
945
+ if response.is_a?(Net::HTTPSuccess)
946
+ { success: true, view_state: extract_view_state_from_ajax(response.body), body: response.body }
947
+ else
948
+ { success: false, error: "HTTP #{response.code}" }
949
+ end
950
+ end
951
+
733
952
  def change_date_ajax(form_id, view_state, date_str, form_data)
734
953
  return { success: false, error: 'No form_id' } unless form_id
735
954
 
@@ -742,6 +961,7 @@ module Truenorth
742
961
  form_data = form_data.dup
743
962
  form_data["#{form_id}:sheetDate"] = date_str
744
963
  form_data["#{form_id}:j_idt57_input"] = date_str
964
+ form_data["#{form_id}:showAllAreasOrTrainers"] = 'true' # Show all courts!
745
965
 
746
966
  form_data.merge!(
747
967
  'javax.faces.partial.ajax' => 'true',
@@ -815,10 +1035,22 @@ module Truenorth
815
1035
 
816
1036
  response = post_ajax(ajax_url, form_data)
817
1037
  if response.is_a?(Net::HTTPSuccess)
818
- body = response.body.downcase
819
- if body.include?('success') || body.include?('confirmed') || body.include?('booked')
1038
+ body = response.body
1039
+ body_lower = body.downcase
1040
+
1041
+ # Check for explicit error indicators
1042
+ if body_lower.include?('error') || body_lower.include?('failed') ||
1043
+ body_lower.include?('unable') || body_lower.include?('invalid')
1044
+ log "Booking save returned error indicators in response"
1045
+ { success: false, error: 'Booking save failed' }
1046
+ # Check for success indicators OR assume success if reasonable response
1047
+ elsif body_lower.include?('success') || body_lower.include?('confirmed') ||
1048
+ body_lower.include?('booked') || body_lower.include?('reserved') ||
1049
+ (body.length < 5000 && !body_lower.include?('exception'))
1050
+ log "Booking save appears successful (HTTP 200)"
820
1051
  { success: true, confirmation: 'Booking confirmed' }
821
1052
  else
1053
+ log "Uncertain booking result, response length: #{body.length}"
822
1054
  { success: false, error: 'No confirmation in response' }
823
1055
  end
824
1056
  else
@@ -8,6 +8,7 @@ module Truenorth
8
8
  CONFIG_DIR = File.expand_path('~/.config/truenorth')
9
9
  CONFIG_FILE = File.join(CONFIG_DIR, 'config.yml')
10
10
  CREDENTIALS_FILE = File.join(CONFIG_DIR, 'credentials.yml')
11
+ COOKIES_FILE = File.join(CONFIG_DIR, 'cookies.yml')
11
12
 
12
13
  class << self
13
14
  def load
@@ -62,8 +63,42 @@ module Truenorth
62
63
  CONFIG_DIR
63
64
  end
64
65
 
66
+ def cookies
67
+ load_cookies
68
+ end
69
+
70
+ def save_cookies(cookies_hash)
71
+ FileUtils.mkdir_p(CONFIG_DIR)
72
+ File.write(COOKIES_FILE, YAML.dump({
73
+ 'cookies' => cookies_hash,
74
+ 'timestamp' => Time.now.to_i
75
+ }))
76
+ FileUtils.chmod(0o600, COOKIES_FILE)
77
+ end
78
+
79
+ def clear_cookies
80
+ File.delete(COOKIES_FILE) if File.exist?(COOKIES_FILE)
81
+ end
82
+
65
83
  private
66
84
 
85
+ def load_cookies
86
+ return {} unless File.exist?(COOKIES_FILE)
87
+
88
+ data = YAML.safe_load(File.read(COOKIES_FILE)) || {}
89
+ timestamp = data['timestamp']
90
+
91
+ # Cookies expire after 24 hours
92
+ if timestamp && (Time.now.to_i - timestamp) < 86400
93
+ data['cookies'] || {}
94
+ else
95
+ clear_cookies
96
+ {}
97
+ end
98
+ rescue StandardError
99
+ {}
100
+ end
101
+
67
102
  def load_config
68
103
  return {} unless File.exist?(CONFIG_FILE)
69
104
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Truenorth
4
- VERSION = '0.3.0'
4
+ VERSION = '0.5.0'
5
5
  end
data/lib/truenorth.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative 'truenorth/version'
4
4
  require_relative 'truenorth/client'
5
+ require_relative 'truenorth/browser_client'
5
6
  require_relative 'truenorth/config'
6
7
 
7
8
  module Truenorth
data/truenorth.gemspec CHANGED
@@ -1,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'lib/truenorth/version'
4
+
3
5
  Gem::Specification.new do |spec|
4
6
  spec.name = 'truenorth'
5
- spec.version = '0.3.0'
7
+ spec.version = Truenorth::VERSION
6
8
  spec.authors = ['usiegj00']
7
9
  spec.email = ['112138+usiegj00@users.noreply.github.com']
8
10
 
@@ -28,6 +30,7 @@ Gem::Specification.new do |spec|
28
30
  spec.require_paths = ['lib']
29
31
 
30
32
  spec.add_dependency 'base64', '~> 0.2'
33
+ spec.add_dependency 'ferrum', '~> 0.15'
31
34
  spec.add_dependency 'nokogiri', '~> 1.15'
32
35
  spec.add_dependency 'thor', '~> 1.3'
33
36
  spec.add_dependency 'tty-table', '~> 0.12'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: truenorth
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - usiegj00
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: ferrum
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.15'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.15'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: nokogiri
29
43
  requirement: !ruby/object:Gem::Requirement