journal-cli 1.0.4

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,127 @@
1
+
2
+ A CLI for journaling to structured data, Markdown, and Day One
3
+
4
+ ## Description
5
+
6
+ The `journal` command reads a journal definition and provides command line prompts to fill it out. The results are stored in a JSON database for each journal, and can optionally output to Markdown (individual files per entry, daily digest, or one large file for the journal).
7
+
8
+ ## Installation
9
+
10
+ First, you need [Gum](https://github.com/charmbracelet/gum) installed. The easiest way is with [Homebrew](https://brew.sh/):
11
+
12
+ ```
13
+ $ brew install gum
14
+ ```
15
+
16
+ Use RubyGems to install journal:
17
+
18
+ ```
19
+ $ gem install journal-cli
20
+ ```
21
+
22
+ If you run into errors, try running with the `--user-install` flag:
23
+
24
+ ```
25
+ $ gem install --user-install journal-cli
26
+ ```
27
+
28
+ > I've noticed lately with `asdf` that I have to run `asdf reshim` after installing gems containing binaries.
29
+
30
+ If you want to use Day One with journal, you'll need to [install the Day One CLI](https://dayoneapp.com/guides/tips-and-tutorials/command-line-interface-cli/).
31
+
32
+ ## Configuration
33
+
34
+ A config must be created at `~/.config/journal/journals.yaml`:
35
+
36
+ ```
37
+ $ mkdir -p ~/.config/journal
38
+ $ touch ~/.config/journal/journals.yaml
39
+ ```
40
+
41
+ This file contains a YAML definition of your journal. Each journal gets a top-level key, which is what you'll specify it with on the command line. It gets a few settings, and then you define sections containing questions.
42
+
43
+ ### Weather
44
+
45
+ You can include weather data automatically by setting a question type to 'weather'. In order for this to work, you'll need to define `zip` and `weather_api` keys. `zip` is just your zip code, and `weather_api` is a key from WeatherAPI.com. Sign up [here](https://www.weatherapi.com/) for a free plan, and then visit the [profile page](https://www.weatherapi.com/my/) to see your API key at the top.
46
+
47
+ ### Journal configuration
48
+
49
+ Edit the file at `~/.config/journal/journals.yaml` following this structure:
50
+
51
+ ```yaml
52
+ daily: # journal key, will be used on the command line as `journal daily`
53
+ dayone: true # Enable or disable Day One integration
54
+ journal: Journal # Day One journal to add to (if using Day One integration)
55
+ markdown: daily # Type of Markdown file to create, false to skip (can be daily, individual, or digest)
56
+ title: Daily Journal # Title for every entry, date will be appended where needed
57
+ sections: # Required key
58
+ - title: null # The title for the section. If null, no section header will be created
59
+ key: journal # The key for the data collected, must be one word, alphanumeric characters and _ only
60
+ questions: # Required key
61
+ - prompt: How are you feeling? # The question to ask
62
+ key: journal # alphanumeric characters and _ only, will be nested in section key
63
+ type: multiline # The type of entry expected (numeric, string, or multiline)
64
+ ```
65
+
66
+ Keys must be alphanumeric characters and `_` (underscore) only. Titles and questions can be anything, but if they contain a colon (:), you'll need to quote the string.
67
+
68
+ A more complex configuration file can contain multiple journals with multiple questions defined:
69
+
70
+ ```yaml
71
+ zip: 55987 # Your zip code for weather integration
72
+ weather_api: XXXXXXXXXXXX # Your weatherapi.com API key
73
+ journals: # required key
74
+ mood: # name of the journal
75
+ journal: Mood Journal # Optional, Day One journal to add to
76
+ tags: [checkin] # Optional, array of tags to add to Day One entries
77
+ markdown: individual # Can be daily or individual, any other value will create a single file
78
+ dayone: true # true to log entries to Day One, false to skip
79
+ title: "Mood checkin %M" # The title of the entry. Use %M to insert AM or PM
80
+ sections: # required key
81
+ - title: Weather # Title of the section (will create template sections in Day One)
82
+ key: weather # the key to use in the structured data, will contain all of the answers
83
+ questions: # required key
84
+ - prompt: Current weather # The prompt shown on the command line, will also become a header in the journal entries (Markdown, Day One)
85
+ key: weather.forecast # if a key contains a dot, it will create nested data, e.g. `{ 'weather': { 'forecast': data } }`
86
+ type: weather # Set this to weather for weather data
87
+ - title: Health # New section
88
+ key: health
89
+ questions:
90
+ - prompt: Health rating
91
+ key: health.rating
92
+ type: numeric # type can be numeric, string, or multiline
93
+ min: 1 # Only need min/max definitions on numeric types (defaults 1-5)
94
+ max: 5
95
+ - prompt: Health notes
96
+ key: health.notes
97
+ type: multiline
98
+ - title: Journal # New section
99
+ key: journal
100
+ questions:
101
+ - prompt: Daily notes
102
+ key: notes
103
+ type: multiline
104
+ daily: # New journal
105
+ journal: Journal
106
+ markdown: daily
107
+ dayone: true
108
+ title: Daily Journal
109
+ sections:
110
+ - title: null
111
+ key: journal
112
+ questions:
113
+ - prompt: How are you feeling?
114
+ key: journal
115
+ type: multiline
116
+ ```
117
+
118
+ A journal must contain a `sections` key, and each section must contain a `questions` key with an array of questions. Each question must (at minimum) have a `prompt`, `key`, and `type`.
119
+
120
+ ## Usage
121
+
122
+ Once your configuration file is set up, you can just run `journal JOURNAL_KEY` to begin prompting for the answers to the configured questions.
123
+
124
+ Answers will always be written to `~/.local/share/journal/[KEY].json` (where [KEY] is the journal key, one data file for each journal). If you've specified `daily` or `individual` Markdown formats, entries will be written to Markdown files in `~/.local/share/journal/entries/[KEY]`, either in a `%Y-%m-%d.md` file (daily), or in timestamped individual files. If `digest` is specified for the `markdown` key, a single file will be created at `~/.local/share/journal/[KEY].md`.
125
+
126
+ At present there's no tool for querying the dataset created. You just need to parse the JSON and use your language of choice to extract the data. Numeric entries are stored as numbers, and every entry is timestamped, so you should be able to do some advanced analysis once you have enough data.
127
+
data/README.rdoc ADDED
@@ -0,0 +1,6 @@
1
+ = Journal
2
+
3
+ A command line tool for journaling to JSON, Markdown, and Day One.
4
+
5
+
6
+
data/Rakefile ADDED
@@ -0,0 +1,85 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+ require 'rdoc/task'
4
+ require 'standard/rake'
5
+ require 'yard'
6
+
7
+ Rake::RDocTask.new do |rd|
8
+ rd.main = "README.rdoc"
9
+ rd.rdoc_files.include("README.rdoc", "lib/**/*.rb", "bin/**/*")
10
+ rd.title = 'Journal'
11
+ end
12
+
13
+ YARD::Rake::YardocTask.new do |t|
14
+ t.files = ['lib/na/*.rb']
15
+ t.options = ['--markup-provider=redcarpet', '--markup=markdown', '--no-private', '-p', 'yard_templates']
16
+ # t.stats_options = ['--list-undoc']
17
+ end
18
+
19
+ RSpec::Core::RakeTask.new(:spec) do |t|
20
+ t.rspec_opts = "--pattern {spec,lib}/**/*_spec.rb"
21
+ end
22
+
23
+ task default: %i[test]
24
+
25
+ desc 'Alias for build'
26
+ task :package => :build
27
+
28
+ task test: "spec"
29
+ task lint: "standard"
30
+ task format: "standard:fix"
31
+
32
+ desc "Open an interactive ruby console"
33
+ task :console do
34
+ require "irb"
35
+ require "bundler/setup"
36
+ require "journal-cli"
37
+ ARGV.clear
38
+ IRB.start
39
+ end
40
+
41
+ desc 'Development version check'
42
+ task :ver do
43
+ gver = `git ver`
44
+ cver = IO.read(File.join(File.dirname(__FILE__), 'CHANGELOG.md')).match(/^#+ (\d+\.\d+\.\d+(\w+)?)/)[1]
45
+ res = `grep VERSION lib/journal-cli/version.rb`
46
+ version = res.match(/VERSION *= *['"](\d+\.\d+\.\d+(\w+)?)/)[1]
47
+ puts "git tag: #{gver}"
48
+ puts "version.rb: #{version}"
49
+ puts "changelog: #{cver}"
50
+ end
51
+
52
+ desc 'Changelog version check'
53
+ task :cver do
54
+ puts IO.read(File.join(File.dirname(__FILE__), 'CHANGELOG.md')).match(/^#+ (\d+\.\d+\.\d+(\w+)?)/)[1]
55
+ end
56
+
57
+ desc 'Bump incremental version number'
58
+ task :bump, :type do |_, args|
59
+ args.with_defaults(type: 'inc')
60
+ version_file = 'lib/journal-cli/version.rb'
61
+ content = IO.read(version_file)
62
+ content.sub!(/VERSION = '(?<major>\d+)\.(?<minor>\d+)\.(?<inc>\d+)(?<pre>\S+)?'/) do
63
+ m = Regexp.last_match
64
+ major = m['major'].to_i
65
+ minor = m['minor'].to_i
66
+ inc = m['inc'].to_i
67
+ pre = m['pre']
68
+
69
+ case args[:type]
70
+ when /^maj/
71
+ major += 1
72
+ minor = 0
73
+ inc = 0
74
+ when /^min/
75
+ minor += 1
76
+ inc = 0
77
+ else
78
+ inc += 1
79
+ end
80
+
81
+ $stdout.puts "At version #{major}.#{minor}.#{inc}#{pre}"
82
+ "VERSION = '#{major}.#{minor}.#{inc}#{pre}'"
83
+ end
84
+ File.open(version_file, 'w+') { |f| f.puts content }
85
+ end
data/bin/journal ADDED
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift File.join(__dir__, '..', 'lib')
4
+ require 'journal-cli'
5
+
6
+ raise ArgumentError, 'no journal specified' if ARGV.count.zero?
7
+
8
+ case ARGV[0]
9
+ when /(-v|--version)/
10
+ puts "journal v#{Journal::VERSION}"
11
+ Process.exit 0
12
+ when /(help|-h|--help)/
13
+ puts "journal v#{Journal::VERSION}"
14
+ puts
15
+ puts 'Usage: journal [type] [date]'
16
+ puts
17
+ puts 'Available journal types:'
18
+ config = Journal::Checkin.new
19
+ puts(config.config['journals'].keys.map { |k| "- #{k}" })
20
+ Process.exit 0
21
+ end
22
+
23
+ journal = ARGV.shift
24
+
25
+ date = if ARGV.length.positive?
26
+ Chronic.parse(ARGV.join(' '), future: false)
27
+ else
28
+ Time.now
29
+ end
30
+ checkin = Journal::Checkin.new
31
+ checkin.start(journal, date)
32
+ checkin.go
@@ -0,0 +1,262 @@
1
+ module Journal
2
+ # Main class
3
+ class Checkin
4
+ attr_reader :key, :date, :data, :config, :journal, :title, :output
5
+
6
+ def initialize
7
+ config = File.expand_path('~/.config/journal/journals.yaml')
8
+ raise StandardError, 'No journals configured' unless File.exist?(config)
9
+
10
+ @config = YAML.load(IO.read(config))
11
+ end
12
+
13
+ def start(journal, date)
14
+ @key = journal
15
+ @output = []
16
+ @date = date
17
+
18
+ raise StandardError, "No journal with key #{@key} found" unless @config['journals'].key? @key
19
+
20
+ @journal = @config['journals'][@key]
21
+
22
+ @data = {}
23
+ meridian = @date.hour < 13 ? 'AM' : 'PM'
24
+ @title = @journal['title'].sub(/%M/, meridian)
25
+ end
26
+
27
+ def title(string)
28
+ @output << "\n## #{string}\n" unless string.nil?
29
+ end
30
+
31
+ def header(string)
32
+ @output << "\n##### #{string}\n" unless string.nil?
33
+ end
34
+
35
+ def section(string)
36
+ @output << "\n###### #{string}\n" unless string.nil?
37
+ end
38
+
39
+ def newline
40
+ @output << "\n"
41
+ end
42
+
43
+ def hr
44
+ @output << "\n---\n"
45
+ end
46
+
47
+ def ask_question(q)
48
+ res = case q['type']
49
+ when /^(int|num)/i
50
+ min = q['min'] || 1
51
+ max = q['max'] || 5
52
+ get_number(q['prompt'], min: min, max: max)
53
+ when /^(text|string|line)/i
54
+ puts q['prompt']
55
+ add_prompt = q['secondary_prompt'] || nil
56
+ get_line(q['prompt'], add_prompt: add_prompt)
57
+ when /^(weather|forecast)/i
58
+ Weather.new(@config['weather_api'], @config['zip'])
59
+ when /^multi/
60
+ puts q['prompt']
61
+ add_prompt = q['secondary_prompt'] || nil
62
+ get_lines(q['prompt'], add_prompt: add_prompt)
63
+ end
64
+
65
+ res
66
+ end
67
+
68
+ def go
69
+ results = Data.new(@journal['questions'])
70
+ @journal['sections'].each do |s|
71
+ results[s['key']] = {
72
+ title: s['title'],
73
+ answers: {}
74
+ }
75
+
76
+ s['questions'].each do |q|
77
+ if q['key'] =~ /\./
78
+ res = results[s['key']][:answers]
79
+ keys = q['key'].split(/\./)
80
+ keys.each_with_index do |key, i|
81
+ next if i == keys.count - 1
82
+
83
+ res[key] = {} unless res.key?(key)
84
+ res = res[key]
85
+ end
86
+
87
+ res[keys.last] = ask_question(q)
88
+ else
89
+ results[s['key']][:answers][q['key']] = ask_question(q)
90
+ end
91
+ end
92
+ end
93
+
94
+ @data = results
95
+
96
+ if @journal['dayone']
97
+ cmd = ['dayone2']
98
+ cmd << %(-j "#{@journal['journal']}") if @journal.key?('journal')
99
+ cmd << %(-t #{@journal['tags'].join(' ')}) if @journal.key?('tags')
100
+ cmd << %(-date "#{@date.strftime('%Y-%m-%d %I:%M %p')}")
101
+ `echo #{Shellwords.escape(to_markdown)} | #{cmd.join(' ')} -- new`
102
+ end
103
+
104
+ if @journal['markdown']
105
+ if @journal['markdown'] =~ /^da(y|ily)/
106
+ dir = File.expand_path("~/.local/share/journal/entries/#{@key}")
107
+ FileUtils.mkdir_p(dir) unless File.directory?(dir)
108
+ filename = "#{@date.strftime('%Y-%m-%d')}.md"
109
+ target = File.join(dir, filename)
110
+ if File.exist? target
111
+ File.open(target, 'a') { |f| f.puts to_markdown(yaml: false, title: true, date: false, time: true) }
112
+ else
113
+ File.open(target, 'w') { |f| f.puts to_markdown(yaml: true, title: true, date: false, time: true) }
114
+ end
115
+ elsif @journal['markdown'] =~ /^(ind|separate)/
116
+ dir = File.expand_path("~/.local/share/journal/entries/#{@key}")
117
+ FileUtils.mkdir_p(dir) unless File.directory?(dir)
118
+ filename = @date.strftime('%Y-%m-%d_%H:%M.md')
119
+ File.open(File.join(dir, filename), 'w') { |f| f.puts to_markdown(yaml: true, title: true) }
120
+ else
121
+ dir = File.expand_path('~/.local/share/journal/entries/')
122
+ FileUtils.mkdir_p(dir) unless File.directory?(dir)
123
+ filename = "#{@key}.md"
124
+ File.open(File.join(dir, filename), 'a') do |f|
125
+ f.puts
126
+ f.puts "## #{@title} #{@date.strftime('%x %X')}"
127
+ f.puts
128
+ f.puts to_markdown(yaml: false, title: false)
129
+ end
130
+ end
131
+ end
132
+
133
+ save_data
134
+ end
135
+
136
+ def print_answer(prompt, type, key, data)
137
+ case type
138
+ when /^(weather|forecast)/
139
+ header prompt
140
+ @output << data[key].to_markdown
141
+ hr
142
+ when /^(int|num)/
143
+ @output << "#{prompt}: #{data[key]} " unless data[key].nil?
144
+ else
145
+ unless data[key].strip.empty?
146
+ header prompt
147
+ @output << data[key]
148
+ end
149
+ hr
150
+ end
151
+ end
152
+
153
+ def to_markdown(yaml: false, title: false, date: false, time: false)
154
+ @output = []
155
+
156
+ if yaml
157
+ @output << <<~EOYAML
158
+ ---
159
+ title: #{@title}
160
+ date: #{@date.strftime('%x %X')}
161
+ ---
162
+
163
+ EOYAML
164
+ end
165
+
166
+ if title
167
+ if date || time
168
+ fmt = ''
169
+ fmt += '%x' if date
170
+ fmt += '%X' if time
171
+ title "#{@title} #{@date.strftime(fmt)}"
172
+ else
173
+ title @title
174
+ end
175
+ end
176
+
177
+ @journal['sections'].each do |s|
178
+ section s['title']
179
+
180
+ s['questions'].each do |q|
181
+ if q['key'] =~ /\./
182
+ res = @data[s['key']][:answers].dup
183
+ keys = q['key'].split(/\./)
184
+ keys.each_with_index do |key, i|
185
+ next if i == keys.count - 1
186
+
187
+ res = res[key]
188
+ end
189
+ print_answer(q['prompt'], q['type'], keys.last, res)
190
+ else
191
+ print_answer(q['prompt'], q['type'], q['key'], @data[s['key']][:answers])
192
+ end
193
+ end
194
+ end
195
+
196
+ @output.join("\n")
197
+ end
198
+
199
+ def save_data
200
+ db = File.expand_path("~/.local/share/journal/#{@key}.json")
201
+ data = if File.exist?(db)
202
+ JSON.parse(IO.read(db))
203
+ else
204
+ []
205
+ end
206
+ date = @date.utc
207
+ output = {}
208
+
209
+ @data.each do |k, v|
210
+ v[:answers].each do |q, a|
211
+ if a.is_a? Hash
212
+ output[q] = {}
213
+ a.each do |key, value|
214
+ output[q][key] = case value.class.to_s
215
+ when /Weather/
216
+ { 'high' => value.data[:high], 'low' => value.data[:low], 'condition' => value.data[:condition] }
217
+ else
218
+ value
219
+ end
220
+ end
221
+ else
222
+ output[q] = a
223
+ end
224
+ end
225
+ end
226
+ data << { 'date' => date, 'data' => output }
227
+ File.open(db, 'w') { |f| f.puts JSON.pretty_generate(data) }
228
+ end
229
+
230
+ def get_number(prompt, min: 1, max: 5)
231
+ puts "#{prompt} (#{min}-#{max})"
232
+ res = `gum input --placeholder "#{prompt} (#{min}-#{max})"`.strip
233
+ return nil if res.strip.empty?
234
+
235
+ res = res.to_i
236
+
237
+ res = get_number(prompt, min: min, max: max) if res < min || res > max
238
+ res
239
+ end
240
+
241
+ def get_line(prompt, add_prompt: nil)
242
+ output = []
243
+ puts prompt
244
+ line = `gum input --placeholder "#{prompt} (blank to end editing)"`
245
+ return output.join("\n") if line =~ /^ *$/
246
+
247
+ output << line
248
+ output << get_line(add_prompt, add_prompt: add_prompt) if add_prompt
249
+ output.join("\n")
250
+ end
251
+
252
+ def get_lines(prompt, add_prompt: nil)
253
+ output = []
254
+ line = `gum write --placeholder "#{prompt}" --width 80 --char-limit 0`
255
+ return output.join("\n") if line.strip.empty?
256
+
257
+ output << line
258
+ output << get_lines(add_prompt, add_prompt: add_prompt) if add_prompt
259
+ output.join("\n")
260
+ end
261
+ end
262
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Journal
4
+ # Data handler
5
+ class Data < Hash
6
+ attr_reader :questions
7
+
8
+ def initialize(questions)
9
+ @questions = questions
10
+ super
11
+ end
12
+
13
+ def to_data
14
+ output = {}
15
+ @questions.each do |q|
16
+ output[q['key']] = self[q['key']]
17
+ end
18
+ output
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Journal
4
+ VERSION = '1.0.4'
5
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Journal
4
+ class Weather
5
+ attr_reader :data
6
+
7
+ def initialize(api, zip)
8
+ res = `curl -SsL 'http://api.weatherapi.com/v1/forecast.json?key=#{api}&q=#{zip}&aqi=no'`
9
+ data = JSON.parse(res)
10
+
11
+ raise StandardError, 'invalid JSON response' if data.nil?
12
+
13
+ raise StandardError, 'missing conditions' unless data['current']
14
+
15
+ curr_temp = data['current']['temp_f']
16
+ curr_condition = data['current']['condition']['text']
17
+
18
+ raise StandardError, 'mising forecast' unless data['forecast']
19
+
20
+ forecast = data['forecast']['forecastday'][0]
21
+
22
+ day = forecast['date']
23
+ high = forecast['day']['maxtemp_f']
24
+ low = forecast['day']['mintemp_f']
25
+ condition = forecast['day']['condition']['text']
26
+
27
+ hours = forecast['hour']
28
+ temps = [
29
+ { temp: hours[8]['temp_f'], condition: hours[8]['condition']['text'] },
30
+ { temp: hours[10]['temp_f'], condition: hours[10]['condition']['text'] },
31
+ { temp: hours[12]['temp_f'], condition: hours[12]['condition']['text'] },
32
+ { temp: hours[14]['temp_f'], condition: hours[14]['condition']['text'] },
33
+ { temp: hours[16]['temp_f'], condition: hours[16]['condition']['text'] },
34
+ { temp: hours[18]['temp_f'], condition: hours[18]['condition']['text'] },
35
+ { temp: hours[19]['temp_f'], condition: hours[20]['condition']['text'] }
36
+ ]
37
+
38
+ @data = {
39
+ day: day,
40
+ high: high,
41
+ low: low,
42
+ temp: curr_temp,
43
+ condition: condition,
44
+ current_condition: curr_condition,
45
+ temps: temps
46
+ }
47
+ end
48
+
49
+ def to_data
50
+ {
51
+ high: high,
52
+ low: low,
53
+ condition: curr_condition
54
+ }
55
+ end
56
+
57
+ def to_markdown
58
+ output = []
59
+
60
+ output << "Forecast for #{@data[:day]}: #{@data[:condition]} #{@data[:high]}/#{@data[:low]} "
61
+ output << "Currently: #{@data[:temp]} and #{@data[:current_condition]}"
62
+ output << ''
63
+
64
+ # Hours
65
+ hours_text = %w[8am 10am 12pm 2pm 4pm 6pm 8pm]
66
+ step_out = ['|']
67
+ @data[:temps].each_with_index do |_h, i|
68
+ width = @data[:temps][i][:condition].length + 1
69
+ step_out << format("%#{width}s |", hours_text[i])
70
+ end
71
+
72
+ output << step_out.join('')
73
+
74
+ # table separator
75
+ step_out = ['|']
76
+ @data[:temps].each do |temp|
77
+ width = temp[:condition].length + 1
78
+ step_out << "#{'-' * width}-|"
79
+ end
80
+
81
+ output << step_out.join('')
82
+
83
+ # Conditions
84
+ step_out = ['|']
85
+ @data[:temps].each do |temp|
86
+ step_out << format(' %s |', temp[:condition])
87
+ end
88
+
89
+ output << step_out.join('')
90
+
91
+ # Temps
92
+ step_out = ['|']
93
+ @data[:temps].each do |temp|
94
+ width = temp[:condition].length + 1
95
+ step_out << format("%#{width}s |", temp[:temp])
96
+ end
97
+
98
+ output << step_out.join('')
99
+
100
+ output.join("\n")
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+ require 'shellwords'
5
+ require 'json'
6
+ require 'yaml'
7
+ require 'chronic'
8
+ require 'fileutils'
9
+
10
+ require_relative "journal-cli/version"
11
+ require_relative "journal-cli/data"
12
+ require_relative "journal-cli/weather"
13
+ require_relative "journal-cli/checkin"