flycal-cli 0.7.3 → 0.7.7

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.
@@ -0,0 +1,229 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/date"
4
+ require "active_support/core_ext/integer"
5
+ require "active_support/core_ext/time"
6
+
7
+ module FlycalCli
8
+ module Pipeline
9
+ # Second pipeline layer: group events and compute aggregate metrics.
10
+ #
11
+ # Default group_by from timeframe:
12
+ # day — timeframe <= 7 days
13
+ # week — timeframe > 7 days and <= 30 days
14
+ # month — timeframe > 30 days
15
+ #
16
+ # Override with params[:group_by_option] / --groupBy:
17
+ # day | week | month | description
18
+ class Aggregator
19
+ HOURS_PER_WORKING_DAY = 8
20
+ ALLOWED_GROUP_BY = %w[day week month description].freeze
21
+
22
+ def call(params)
23
+ time_min = params[:time_min]
24
+ time_max = params[:time_max]
25
+ events = Array(params[:events])
26
+
27
+ timeframe_days = (time_max - time_min) / 86400.0
28
+ group_by = resolve_group_by(timeframe_days, params[:group_by_option])
29
+
30
+ params[:timeframe_days] = timeframe_days
31
+ params[:group_by] = group_by
32
+
33
+ ranges = events.filter_map do |ev|
34
+ next if ev[:start_at].nil? || ev[:end_at].nil?
35
+
36
+ [ev[:start_at], ev[:end_at]]
37
+ end
38
+
39
+ total_minutes = ranges.sum { |s, e| (e - s) / 60.0 }
40
+ params[:totals] = {
41
+ event_count: events.size,
42
+ total_minutes: total_minutes,
43
+ hours: (total_minutes / 60.0).floor,
44
+ minutes: (total_minutes % 60).round,
45
+ working_days: (total_minutes / 60.0 / HOURS_PER_WORKING_DAY).round(1)
46
+ }
47
+
48
+ params[:groups] =
49
+ case group_by
50
+ when "week" then weekly_groups(events, ranges, time_min, time_max)
51
+ when "month" then monthly_groups(events, ranges, time_min, time_max)
52
+ when "description" then description_groups(events, params[:description])
53
+ else daily_groups(events, ranges, time_min, time_max)
54
+ end
55
+
56
+ params
57
+ end
58
+
59
+ def self.resolve_group_by(timeframe_days, explicit = nil)
60
+ key = explicit.to_s.strip.downcase
61
+ unless key.empty?
62
+ unless ALLOWED_GROUP_BY.include?(key)
63
+ raise FlycalCli::Error,
64
+ "Unsupported groupBy #{explicit.inspect}. Available: #{ALLOWED_GROUP_BY.join(", ")}"
65
+ end
66
+ return key
67
+ end
68
+
69
+ if timeframe_days > 30
70
+ "month"
71
+ elsif timeframe_days > 7
72
+ "week"
73
+ else
74
+ "day"
75
+ end
76
+ end
77
+
78
+ private
79
+
80
+ def resolve_group_by(timeframe_days, explicit)
81
+ self.class.resolve_group_by(timeframe_days, explicit)
82
+ end
83
+
84
+ def daily_groups(events, ranges, time_min, time_max)
85
+ groups = []
86
+ index = 1
87
+ current = time_min.to_date
88
+ end_date = time_max.to_date
89
+
90
+ while current <= end_date
91
+ day_start = [current.to_time, time_min].max
92
+ day_end = [(current + 1).to_time, time_max].min
93
+ mins = minutes_in_period(ranges, day_start, day_end)
94
+ groups << build_group(
95
+ index: index,
96
+ key: current.strftime("%Y-%m-%d"),
97
+ start_at: day_start,
98
+ end_at: day_end,
99
+ period_label_end: current,
100
+ total_minutes: mins,
101
+ events: events_in_period(events, day_start, day_end)
102
+ )
103
+ index += 1
104
+ current += 1
105
+ end
106
+
107
+ groups
108
+ end
109
+
110
+ def weekly_groups(events, ranges, time_min, time_max)
111
+ groups = []
112
+ index = 1
113
+ current = time_min.to_date.beginning_of_week(:monday)
114
+
115
+ while current.to_time < time_max
116
+ week_start = [current.to_time, time_min].max
117
+ week_end_date = current.end_of_week(:monday)
118
+ week_end = [week_end_date.to_time + 1.day, time_max].min
119
+ mins = minutes_in_period(ranges, week_start, week_end)
120
+ groups << build_group(
121
+ index: index,
122
+ key: "W#{index}",
123
+ start_at: week_start,
124
+ end_at: week_end,
125
+ period_label_end: week_end_date,
126
+ total_minutes: mins,
127
+ events: events_in_period(events, week_start, week_end)
128
+ )
129
+ index += 1
130
+ current += 7
131
+ end
132
+
133
+ groups
134
+ end
135
+
136
+ def monthly_groups(events, ranges, time_min, time_max)
137
+ groups = []
138
+ index = 1
139
+ current = time_min.to_date.beginning_of_month
140
+ end_date = time_max.to_date
141
+
142
+ while current <= end_date
143
+ month_start = current.beginning_of_month.to_time
144
+ month_end = (current.end_of_month + 1.day).to_time
145
+ period_start = [month_start, time_min].max
146
+ period_end = [month_end, time_max].min
147
+ mins = minutes_in_period(ranges, period_start, period_end)
148
+ last_day = period_end.to_date - 1
149
+ groups << build_group(
150
+ index: index,
151
+ key: current.strftime("%Y-%m"),
152
+ start_at: period_start,
153
+ end_at: period_end,
154
+ period_label_end: last_day,
155
+ total_minutes: mins,
156
+ month_name: "#{Locale.month_name(current)} #{current.year}",
157
+ events: events_in_period(events, period_start, period_end)
158
+ )
159
+ index += 1
160
+ current = current.next_month.beginning_of_month
161
+ end
162
+
163
+ groups
164
+ end
165
+
166
+ def description_groups(events, description_query)
167
+ patterns = DescriptionQuery.patterns(description_query)
168
+ if patterns.empty?
169
+ raise FlycalCli::Error,
170
+ "groupBy description requires --description with at least one term (e.g. rui|solver)."
171
+ end
172
+
173
+ patterns.each_with_index.map do |pattern, idx|
174
+ matched = events.select do |ev|
175
+ DescriptionQuery.match_term?(ev[:summary], ev[:description], pattern)
176
+ end
177
+ minutes = matched.sum { |ev| ev[:duration_minutes].to_f }
178
+ build_group(
179
+ index: idx + 1,
180
+ key: pattern,
181
+ start_at: nil,
182
+ end_at: nil,
183
+ period_label_end: nil,
184
+ total_minutes: minutes,
185
+ events: matched,
186
+ description: pattern
187
+ )
188
+ end
189
+ end
190
+
191
+ def build_group(index:, key:, start_at:, end_at:, period_label_end:, total_minutes:, events:,
192
+ month_name: nil, description: nil)
193
+ {
194
+ index: index,
195
+ key: key,
196
+ start_at: start_at,
197
+ end_at: end_at,
198
+ period_label_end: period_label_end,
199
+ month_name: month_name,
200
+ description: description,
201
+ event_count: events.size,
202
+ total_minutes: total_minutes,
203
+ hours: (total_minutes / 60.0).round(1),
204
+ working_days: (total_minutes / 60.0 / HOURS_PER_WORKING_DAY).round(1),
205
+ events: events
206
+ }
207
+ end
208
+
209
+ def events_in_period(events, period_start, period_end)
210
+ events.select do |ev|
211
+ start_at = ev[:start_at]
212
+ end_at = ev[:end_at]
213
+ next false if start_at.nil? || end_at.nil?
214
+
215
+ end_at > period_start && start_at < period_end
216
+ end
217
+ end
218
+
219
+ def minutes_in_period(ranges, period_start, period_end)
220
+ ranges.sum do |ev_start, ev_end|
221
+ overlap_start = [ev_start, period_start].max
222
+ overlap_end = [ev_end, period_end].min
223
+ overlap_sec = overlap_end - overlap_start
224
+ overlap_sec > 0 ? overlap_sec / 60.0 : 0
225
+ end
226
+ end
227
+ end
228
+ end
229
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module FlycalCli
6
+ module Pipeline
7
+ # Pretty-printed JSON output for search (API / tooling friendly).
8
+ class JsonRenderer < Renderer
9
+ def render(params)
10
+ payload = {
11
+ "params" => build_params(params),
12
+ "info" => build_info(params),
13
+ "items" => Array(params[:events]).map { |ev| serialize_event(ev) },
14
+ "groups" => build_groups(params)
15
+ }
16
+
17
+ JSON.pretty_generate(payload) + "\n"
18
+ end
19
+
20
+ private
21
+
22
+ def build_params(params)
23
+ out = {
24
+ "command" => params[:command] || "search",
25
+ "from" => format_time(params[:time_min]),
26
+ "to" => format_time(params[:time_max]),
27
+ "calendar" => params[:calendar],
28
+ "calendar_ids" => Array(params[:calendar_ids]),
29
+ "description" => params[:description],
30
+ "format" => params[:format] || "json",
31
+ "locale" => params[:locale] || Locale.current_locale,
32
+ "group_by" => params[:group_by],
33
+ "group_by_option" => blank_to_nil(params[:group_by_option]),
34
+ "from_option" => blank_to_nil(params[:from_option]),
35
+ "to_option" => blank_to_nil(params[:to_option]),
36
+ "in_option" => blank_to_nil(params[:in_option])
37
+ }
38
+
39
+ if params[:use_mock]
40
+ out["use_mock"] = true
41
+ out["mock_calendar"] = params[:mock_calendar]
42
+ out["mock_template"] = params[:mock_template]
43
+ out["mock_seed"] = params[:mock_seed]
44
+ end
45
+
46
+ out
47
+ end
48
+
49
+ def build_info(params)
50
+ totals = params[:totals] || {}
51
+ total_minutes = totals[:total_minutes].to_f
52
+ info = {
53
+ "events_found" => totals[:event_count].to_i,
54
+ "from" => format_time(params[:time_min]),
55
+ "to" => format_time(params[:time_max]),
56
+ "total_hours" => (total_minutes / 60.0).round(2),
57
+ "total_working_days" => totals[:working_days].to_f
58
+ }
59
+ info["mock_seed"] = params[:mock_seed] if params[:use_mock] && !params[:mock_seed].nil?
60
+ info
61
+ end
62
+
63
+ def build_groups(params)
64
+ type = params[:group_by].to_s
65
+ Array(params[:groups]).map do |group|
66
+ {
67
+ "type" => type,
68
+ "key" => group[:key],
69
+ "index" => group[:index],
70
+ "total_hours" => group[:hours],
71
+ "total_working_days" => group[:working_days],
72
+ "events_found" => group[:event_count] || Array(group[:events]).size,
73
+ "items" => Array(group[:events]).map { |ev| serialize_event(ev) }
74
+ }.tap do |g|
75
+ if type == "description"
76
+ g["description"] = group[:description] || group[:key]
77
+ else
78
+ g["from"] = format_group_boundary(group[:start_at])
79
+ g["to"] = format_group_boundary(group[:end_at])
80
+ end
81
+ g["month_name"] = group[:month_name] if group[:month_name]
82
+ end
83
+ end
84
+ end
85
+
86
+ # Shape inspired by Google Calendar API event resources.
87
+ def serialize_event(ev)
88
+ raw = ev[:raw]
89
+ payload = {
90
+ "summary" => ev[:summary],
91
+ "description" => ev[:description],
92
+ "start" => boundary_hash(ev[:start_at], all_day: ev[:all_day]),
93
+ "end" => boundary_hash(ev[:end_at], all_day: ev[:all_day]),
94
+ "calendarId" => ev[:calendar_id],
95
+ "calendarSummary" => ev[:calendar_name]
96
+ }
97
+
98
+ if raw
99
+ payload["id"] = raw.id if raw.respond_to?(:id) && raw.id
100
+ payload["status"] = raw.status if raw.respond_to?(:status) && raw.status
101
+ payload["htmlLink"] = raw.html_link if raw.respond_to?(:html_link) && raw.html_link
102
+ payload["created"] = format_time(raw.created) if raw.respond_to?(:created) && raw.created
103
+ payload["updated"] = format_time(raw.updated) if raw.respond_to?(:updated) && raw.updated
104
+ payload["location"] = raw.location if raw.respond_to?(:location) && raw.location
105
+ payload["iCalUID"] = raw.i_cal_uid if raw.respond_to?(:i_cal_uid) && raw.i_cal_uid
106
+ end
107
+
108
+ payload.compact
109
+ end
110
+
111
+ def boundary_hash(time, all_day:)
112
+ return nil if time.nil?
113
+
114
+ if all_day
115
+ { "date" => time.to_date.iso8601 }
116
+ else
117
+ { "dateTime" => format_time(time) }
118
+ end
119
+ end
120
+
121
+ def format_time(value)
122
+ return nil if value.nil?
123
+ return value.iso8601 if value.respond_to?(:iso8601)
124
+
125
+ value.to_s
126
+ end
127
+
128
+ # Compact local datetime used on group boundaries: YYYYMMDDTHHMMSS
129
+ def format_group_boundary(value)
130
+ return nil if value.nil?
131
+
132
+ t = value.respond_to?(:to_time) ? value.to_time : value
133
+ t.strftime("%Y%m%dT%H%M%S")
134
+ end
135
+
136
+ def blank_to_nil(value)
137
+ str = value.to_s
138
+ str.empty? ? nil : str
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlycalCli
4
+ module Pipeline
5
+ # Mutable bag of CLI + derived parameters passed through the pipeline.
6
+ class Params
7
+ def initialize(initial = {})
8
+ @data = {}
9
+ initial.each { |k, v| self[k] = v }
10
+ end
11
+
12
+ def [](key)
13
+ @data[key.to_sym]
14
+ end
15
+
16
+ def []=(key, value)
17
+ @data[key.to_sym] = value
18
+ end
19
+
20
+ def fetch(key, default = nil, &block)
21
+ if block
22
+ @data.fetch(key.to_sym, &block)
23
+ else
24
+ @data.fetch(key.to_sym, default)
25
+ end
26
+ end
27
+
28
+ def key?(key)
29
+ @data.key?(key.to_sym)
30
+ end
31
+
32
+ def merge!(other)
33
+ other.each { |k, v| self[k] = v }
34
+ self
35
+ end
36
+
37
+ def to_h
38
+ @data.dup
39
+ end
40
+
41
+ def each(&block)
42
+ @data.each(&block)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlycalCli
4
+ module Pipeline
5
+ # Third pipeline layer: render aggregated results to a chosen format.
6
+ class Renderer
7
+ FORMATS = {
8
+ "text" => "FlycalCli::Pipeline::TextRenderer",
9
+ "json" => "FlycalCli::Pipeline::JsonRenderer"
10
+ }.freeze
11
+
12
+ def self.for(format)
13
+ key = format.to_s.strip.downcase
14
+ key = "text" if key.empty?
15
+ class_name = FORMATS[key]
16
+ raise FlycalCli::Error, "Unsupported format #{format.inspect}. Available: #{FORMATS.keys.join(", ")}" unless class_name
17
+
18
+ Object.const_get(class_name).new
19
+ end
20
+
21
+ # @return [String] rendered output
22
+ def render(params)
23
+ raise NotImplementedError, "#{self.class}#render must be implemented"
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlycalCli
4
+ module Pipeline
5
+ # First pipeline layer: fetch calendar events for the requested timeframe.
6
+ class Retriever
7
+ def initialize(calendar_service)
8
+ @service = calendar_service
9
+ end
10
+
11
+ # Expects params: :calendar_ids, :time_min, :time_max, optional :description
12
+ # Writes: :events (normalized), :calendar_names
13
+ def call(params)
14
+ calendar_ids = Array(params[:calendar_ids])
15
+ time_min = params[:time_min]
16
+ time_max = params[:time_max]
17
+ query = params[:description]
18
+
19
+ calendar_list = @service.list_calendars
20
+ calendar_names = calendar_list.to_h { |c| [c.id, c.summary || c.id] }
21
+ params[:calendar_names] = calendar_names
22
+
23
+ raw = @service.list_all_events(
24
+ calendar_ids,
25
+ time_min: time_min,
26
+ time_max: time_max,
27
+ query: query
28
+ )
29
+
30
+ params[:events] = raw.map { |item| normalize_event(item, calendar_names) }
31
+ params
32
+ end
33
+
34
+ private
35
+
36
+ def normalize_event(item, calendar_names)
37
+ event = item[:event]
38
+ cal_id = item[:calendar_id]
39
+ start_at = to_time(event.start&.date_time || event.start&.date)
40
+ end_at = to_time(event.end&.date_time || event.end&.date)
41
+ minutes =
42
+ if start_at && end_at && end_at > start_at
43
+ (end_at - start_at) / 60.0
44
+ else
45
+ 0.0
46
+ end
47
+
48
+ {
49
+ calendar_id: cal_id,
50
+ calendar_name: calendar_names[cal_id] || cal_id,
51
+ summary: event.summary,
52
+ description: event.description,
53
+ start_at: start_at,
54
+ end_at: end_at,
55
+ duration_minutes: minutes,
56
+ all_day: !event.start&.date.nil? && event.start&.date_time.nil?,
57
+ raw: event
58
+ }
59
+ end
60
+
61
+ def to_time(value)
62
+ return nil if value.nil?
63
+ return value if value.is_a?(Time)
64
+ return value.to_time if value.respond_to?(:to_time) && !value.is_a?(String)
65
+
66
+ Time.parse(value.to_s)
67
+ rescue ArgumentError
68
+ nil
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlycalCli
4
+ module Pipeline
5
+ # Orchestrates Retriever → Aggregator → Renderer for search.
6
+ class SearchPipeline
7
+ def initialize(calendar_service)
8
+ @service = calendar_service
9
+ end
10
+
11
+ def run(params)
12
+ Retriever.new(@service).call(params)
13
+ Aggregator.new.call(params)
14
+ Renderer.for(params[:format]).render(params)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlycalCli
4
+ module Pipeline
5
+ # Shell text output — preserves the historical flycal search formatting.
6
+ class TextRenderer < Renderer
7
+ HOURS_PER_WORKING_DAY = Aggregator::HOURS_PER_WORKING_DAY
8
+
9
+ def render(params)
10
+ lines = []
11
+ lines.concat(event_lines(params))
12
+ lines << ""
13
+ lines.concat(summary_lines(params))
14
+ lines.join("\n") + "\n"
15
+ end
16
+
17
+ private
18
+
19
+ def event_lines(params)
20
+ Array(params[:events]).map do |ev|
21
+ start_str = format_datetime(ev[:start_at])
22
+ end_str = format_datetime(ev[:end_at])
23
+ desc = ev[:summary] || "(no title)"
24
+ if ev[:summary].nil? && ev[:description]
25
+ desc = ev[:description].to_s.slice(0, 80)
26
+ end
27
+ "#{ev[:calendar_name]} | #{start_str} | #{end_str} | #{desc}"
28
+ end
29
+ end
30
+
31
+ def summary_lines(params)
32
+ totals = params[:totals] || {}
33
+ time_min = params[:time_min]
34
+ time_max = params[:time_max]
35
+ from_str = time_min.strftime("%a %Y-%m-%d %H:%M")
36
+ to_str = time_max.strftime("%a %Y-%m-%d %H:%M")
37
+
38
+ lines = []
39
+ lines << "---"
40
+ lines << "From: #{from_str} | To: #{to_str}"
41
+ lines << "Events found: #{totals[:event_count]}"
42
+ lines << "Total time occupied: #{format_duration(totals[:total_minutes].to_f)}"
43
+ lines << "Mock seed: #{params[:mock_seed]}" if params[:use_mock] && !params[:mock_seed].nil?
44
+
45
+ case params[:group_by]
46
+ when "week"
47
+ lines << ""
48
+ lines << "By week:"
49
+ lines.concat(week_group_lines(params[:groups]))
50
+ when "month"
51
+ lines << ""
52
+ lines << "By month:"
53
+ lines.concat(month_group_lines(params[:groups]))
54
+ when "day"
55
+ if explicit_group_by?(params, "day")
56
+ lines << ""
57
+ lines << "By day:"
58
+ lines.concat(day_group_lines(params[:groups]))
59
+ end
60
+ when "description"
61
+ lines << ""
62
+ lines << "By description:"
63
+ lines.concat(description_group_lines(params[:groups]))
64
+ end
65
+
66
+ lines
67
+ end
68
+
69
+ def explicit_group_by?(params, value)
70
+ params[:group_by_option].to_s.strip.downcase == value
71
+ end
72
+
73
+ def day_group_lines(groups)
74
+ Array(groups).map do |g|
75
+ day_str = format_date_with_day(g[:start_at])
76
+ " #{bold(g[:index])}. #{day_str}: #{format_hours_and_days(g[:hours], g[:working_days])}"
77
+ end
78
+ end
79
+
80
+ def week_group_lines(groups)
81
+ Array(groups).map do |g|
82
+ start_str = format_date_with_day(g[:start_at])
83
+ end_str = format_date_with_day(g[:period_label_end])
84
+ " #{bold(g[:index])}. #{start_str} - #{end_str}: #{format_hours_and_days(g[:hours], g[:working_days])}"
85
+ end
86
+ end
87
+
88
+ def month_group_lines(groups)
89
+ Array(groups).map do |g|
90
+ start_str = format_date_with_day(g[:start_at])
91
+ end_str = format_date_with_day(g[:period_label_end])
92
+ " #{g[:index]}. #{g[:month_name]} (#{start_str} - #{end_str}): #{format_hours_and_days(g[:hours], g[:working_days])}"
93
+ end
94
+ end
95
+
96
+ def description_group_lines(groups)
97
+ Array(groups).map do |g|
98
+ label = g[:description] || g[:key]
99
+ " #{bold(g[:index])}. #{label}: #{g[:event_count]} events, #{format_hours_and_days(g[:hours], g[:working_days])}"
100
+ end
101
+ end
102
+
103
+ def format_datetime(dt)
104
+ return "-" if dt.nil?
105
+ return dt if dt.is_a?(String)
106
+
107
+ "#{Locale.day_abbr(dt)} #{dt.strftime("%Y-%m-%d %H:%M")}"
108
+ end
109
+
110
+ def format_date_with_day(dt)
111
+ return "-" if dt.nil?
112
+
113
+ t = dt.respond_to?(:to_time) ? dt.to_time : dt
114
+ "#{Locale.day_abbr(t)} #{t.strftime("%Y-%m-%d")}"
115
+ end
116
+
117
+ def format_duration(total_minutes)
118
+ hours = (total_minutes / 60).floor
119
+ mins = (total_minutes % 60).round
120
+ working_days = (total_minutes / 60.0 / HOURS_PER_WORKING_DAY).round(1)
121
+ "#{bold(hours)}h #{bold(mins)}min (#{bold(working_days)} working days)"
122
+ end
123
+
124
+ def format_hours_and_days(hours, working_days)
125
+ "#{bold(hours)}h (#{bold(working_days)} working days)"
126
+ end
127
+
128
+ def bold(str)
129
+ "\e[1m#{str}\e[0m"
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "flycal_cli/pipeline/params"
4
+ require "flycal_cli/pipeline/retriever"
5
+ require "flycal_cli/pipeline/aggregator"
6
+ require "flycal_cli/pipeline/renderer"
7
+ require "flycal_cli/pipeline/text_renderer"
8
+ require "flycal_cli/pipeline/json_renderer"
9
+ require "flycal_cli/pipeline/search_pipeline"
10
+
11
+ module FlycalCli
12
+ module Pipeline
13
+ end
14
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FlycalCli
4
- VERSION = '0.7.3'
4
+ VERSION = '0.7.7'
5
5
  end