mps 0.5.0 → 1.0.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.
data/Gemfile.lock ADDED
@@ -0,0 +1,46 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ mps (1.0.0)
5
+ chronic (~> 0.10.2)
6
+ cli-ui (~> 2.2)
7
+ thor (~> 1.3)
8
+ tty-editor (~> 0.7.0)
9
+
10
+ GEM
11
+ remote: https://rubygems.org/
12
+ specs:
13
+ chronic (0.10.2)
14
+ cli-ui (2.7.0)
15
+ fakefs (2.8.0)
16
+ minitest (5.27.0)
17
+ pastel (0.8.0)
18
+ tty-color (~> 0.5)
19
+ rake (13.4.2)
20
+ thor (1.5.0)
21
+ tty-color (0.6.0)
22
+ tty-cursor (0.7.1)
23
+ tty-editor (0.7.0)
24
+ tty-prompt (~> 0.22)
25
+ tty-prompt (0.23.1)
26
+ pastel (~> 0.8)
27
+ tty-reader (~> 0.8)
28
+ tty-reader (0.9.0)
29
+ tty-cursor (~> 0.7)
30
+ tty-screen (~> 0.8)
31
+ wisper (~> 2.0)
32
+ tty-screen (0.8.2)
33
+ wisper (2.0.1)
34
+
35
+ PLATFORMS
36
+ x86_64-linux
37
+ x86_64-linux-gnu
38
+
39
+ DEPENDENCIES
40
+ fakefs (~> 2.5)
41
+ minitest (~> 5.0)
42
+ mps!
43
+ rake (~> 13.2)
44
+
45
+ BUNDLED WITH
46
+ 2.4.20
data/Rakefile CHANGED
@@ -3,16 +3,17 @@
3
3
  require "bundler/gem_tasks"
4
4
  require "rake/testtask"
5
5
 
6
- Rake::TestTask.new(:default_test) do |t|
6
+ Rake::TestTask.new(:test) do |t|
7
7
  t.libs << "test"
8
8
  t.libs << "lib"
9
9
  t.test_files = FileList["test/**/*_test.rb"]
10
10
  end
11
11
 
12
12
  task default: :test
13
- namespace :test do
14
- desc "Run test task with specified groups of gems bundle"
15
- task :with_groups do
16
- Rake::Task[:default_test].invoke
13
+
14
+ namespace :test do
15
+ desc "Run all tests (alias used by CI)"
16
+ task :with_groups do
17
+ Rake::Task[:test].invoke
17
18
  end
18
19
  end
data/lib/cli/mps.rb CHANGED
@@ -1,321 +1,48 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'thor'
4
- require 'yaml'
5
- require 'json'
6
- require 'csv'
7
- require 'cli/ui'
3
+ require "thor"
4
+ require "yaml"
5
+ require "json"
6
+ require "csv"
7
+ require "cli/ui"
8
8
 
9
9
  module MPS
10
10
  module CLI
11
+ # Root Thor dispatcher. All commands self-register via class_eval from
12
+ # lib/mps/cli/commands/*.rb — adding a command means adding a file there.
11
13
  class MPS < Thor
12
14
  include Thor::Actions
13
15
  class_option :verbose, type: :boolean, default: false
14
16
  class_option :config_path, type: :string, default: ::MPS::Constants::MPS_CONFIG_FILE,
15
17
  desc: "mps config file path"
16
18
  class_option :force, type: :boolean, default: false
17
- default_task :open
18
19
 
20
+ default_task :open
19
21
  VALID_TYPES = %w[task note log reminder].freeze
20
22
 
21
- def self.exit_on_failure?
22
- true
23
+ def self.exit_on_failure? = true
24
+
25
+ # Intercept bare invocation to honour config's default_command.
26
+ def self.start(given_args = ARGV, config = {})
27
+ if given_args.empty?
28
+ begin
29
+ conf = ::MPS::Config.load_conf_hash(::MPS::Constants::MPS_CONFIG_FILE)
30
+ default = conf.fetch(:default_command, "open").to_s
31
+ return super(["list"] + given_args, config) if default == "list"
32
+ rescue StandardError
33
+ # Config may not exist yet — fall through to normal start
34
+ end
35
+ end
36
+ super
23
37
  end
24
38
 
25
- # ── version ────────────────────────────────────────────────────────────
39
+ # ── version (stays here — it's tiny and infrastructure) ───────────────
26
40
 
27
41
  desc "version", "Print version"
28
42
  def version
29
43
  say "mps (v#{::MPS::VERSION})"
30
44
  end
31
45
 
32
- # ── open ───────────────────────────────────────────────────────────────
33
-
34
- desc "open [DATESIGN]", "Open .mps file in editor (default: today)"
35
- def open(datesign = "today")
36
- init
37
- begin
38
- date = ::MPS.get_date(datesign)
39
- store = ::MPS::Store.new(@config.storage_dir)
40
- files = store.find_files(date)
41
- file_path = if files.size > 1
42
- ::CLI::UI::Prompt.ask("#{files.size} files found:") do |h|
43
- files.each { |f| h.option(File.basename(f)) { |_| f } }
44
- end
45
- else
46
- store.find_or_create_path(date)
47
- end
48
- @config.logger.info("Open MPS in text editor\n")
49
- written = ::MPS.open_editor(file_path)
50
- @config.logger.info("Done written Size: #{written} bytes\n")
51
- say_status :written, "#{written} bytes", :green
52
- rescue StandardError => e
53
- raise Thor::Error, e
54
- end
55
- end
56
-
57
- # ── list ───────────────────────────────────────────────────────────────
58
-
59
- desc "list [DATESIGN]", "List elements for a date (default: today)"
60
- method_option :type, type: :string, aliases: "-t",
61
- desc: "Filter by type: task, note, log, reminder"
62
- method_option :tag, type: :string, aliases: "-g", desc: "Filter by tag"
63
- method_option :status, type: :string, aliases: "-s",
64
- desc: "Filter tasks by status: open, done"
65
- method_option :since, type: :string, aliases: "-S",
66
- desc: "Show elements from SINCE up to DATESIGN"
67
- def list(datesign = "today")
68
- init
69
- begin
70
- store = ::MPS::Store.new(@config.storage_dir)
71
- date = ::MPS.get_date(datesign)
72
- dates = options[:since] ? date_range(options[:since], date) : [date.to_date]
73
-
74
- shown = 0
75
- dates.each do |d|
76
- all = store.parse_date(d)
77
- next if all.empty?
78
- say set_color("── #{d.strftime('%Y-%m-%d')} ─────────────", :white) if dates.size > 1
79
- shown += print_tree(all, options)
80
- end
81
- say set_color("(no elements found)", :yellow) if shown.zero?
82
- rescue StandardError => e
83
- raise Thor::Error, e
84
- end
85
- end
86
-
87
- # ── append ─────────────────────────────────────────────────────────────
88
-
89
- desc "append TYPE BODY", "Append an element to today's file without opening Vim"
90
- method_option :tags, type: :string, desc: "Comma-separated tags (e.g. work,release)"
91
- method_option :status, type: :string, desc: "Task status: open (default) or done"
92
- method_option :at, type: :string, desc: "Time for reminders (e.g. '3pm')"
93
- method_option :start_time, type: :string, desc: "Start time for logs (HH:MM)"
94
- method_option :end_time, type: :string, desc: "End time for logs (HH:MM)"
95
- def append(type, *body_parts)
96
- init
97
- begin
98
- type = type.downcase
99
- unless VALID_TYPES.include?(type)
100
- raise Thor::Error, "Unknown type '#{type}'. Valid: #{VALID_TYPES.join(', ')}"
101
- end
102
- body = body_parts.join(" ")
103
- tags = options[:tags]&.split(",")&.map(&:strip) || []
104
- attrs = {}
105
- attrs[:status] = options[:status] if options[:status]
106
- attrs[:at] = options[:at] if options[:at]
107
- attrs[:start] = options[:start_time] if options[:start_time]
108
- attrs[:end] = options[:end_time] if options[:end_time]
109
-
110
- store = ::MPS::Store.new(@config.storage_dir)
111
- path = store.append(type: type, body: body, tags: tags, attrs: attrs)
112
- say_status :appended, "#{type_badge(type)} #{body}", :green
113
- @config.logger.info("Appended #{type} to #{File.basename(path)}\n")
114
- rescue StandardError => e
115
- raise Thor::Error, e
116
- end
117
- end
118
-
119
- # ── search ─────────────────────────────────────────────────────────────
120
-
121
- desc "search QUERY", "Full-text search across all .mps files"
122
- method_option :type, type: :string, aliases: "-t", desc: "Filter by type"
123
- method_option :tag, type: :string, aliases: "-g", desc: "Filter by tag"
124
- method_option :since, type: :string, aliases: "-S", desc: "Search from this date onward"
125
- def search(query)
126
- init
127
- begin
128
- store = ::MPS::Store.new(@config.storage_dir)
129
- since_date = options[:since] ? ::MPS.get_date(options[:since]).to_date : nil
130
- results = store.search(
131
- query,
132
- type_filter: options[:type]&.downcase,
133
- tag_filter: options[:tag],
134
- since_date: since_date
135
- )
136
- if results.empty?
137
- say set_color("No results for '#{query}'", :yellow)
138
- return
139
- end
140
- results.each do |r|
141
- el = r[:element]
142
- tags_str = el.tags.empty? ? "" : " #{set_color("[#{el.tags.join(', ')}]", :white)}"
143
- body_line = el.body_str.strip.lines.first&.strip
144
- say "#{set_color(r[:date_str], :white)} #{type_badge(el.class::SIGNATURE_STAMP)} " \
145
- "#{element_extra(el)}#{body_line}#{tags_str}"
146
- end
147
- say set_color("(#{results.size} result#{results.size == 1 ? '' : 's'})", :white)
148
- rescue StandardError => e
149
- raise Thor::Error, e
150
- end
151
- end
152
-
153
- # ── stats ──────────────────────────────────────────────────────────────
154
-
155
- desc "stats [DATESIGN]", "Show element counts and log durations"
156
- method_option :since, type: :string, aliases: "-S",
157
- desc: "Stats from SINCE up to DATESIGN"
158
- def stats(datesign = "today")
159
- init
160
- begin
161
- store = ::MPS::Store.new(@config.storage_dir)
162
- date = ::MPS.get_date(datesign)
163
- dates = options[:since] ? date_range(options[:since], date) : [date.to_date]
164
-
165
- total = Hash.new(0)
166
- total_log_mins = 0
167
- any = false
168
-
169
- dates.each do |d|
170
- elements = store.parse_date(d).values
171
- .reject { |e| e.is_a?(::MPS::Elements::MPS) }
172
- next if elements.empty?
173
- any = true
174
- counts = elements.group_by { |e| e.class::SIGNATURE_STAMP }.transform_values(&:size)
175
- log_mins = elements.select { |e| e.is_a?(::MPS::Elements::Log) }
176
- .sum { |e| e.duration_minutes || 0 }
177
- tasks = elements.select { |e| e.is_a?(::MPS::Elements::Task) }
178
-
179
- parts = []
180
- if (n = counts["task"])
181
- open_n = tasks.count(&:open?)
182
- done_n = tasks.count(&:done?)
183
- parts << "#{n} task#{n != 1 ? 's' : ''} " \
184
- "(#{set_color("#{open_n} open", :yellow)}, " \
185
- "#{set_color("#{done_n} done", :green)})"
186
- end
187
- parts << "#{counts['note']} note#{counts['note'] != 1 ? 's' : ''}" if counts["note"]
188
- parts << "#{counts['reminder']} reminder#{counts['reminder'] != 1 ? 's':''}" if counts["reminder"]
189
- if (n = counts["log"])
190
- h, m = log_mins.divmod(60)
191
- dur = log_mins > 0 ? " (#{h}h#{m > 0 ? "#{m}m" : ""})" : ""
192
- parts << "#{n} log#{n != 1 ? 's' : ''}#{dur}"
193
- end
194
-
195
- say "#{set_color(d.strftime('%Y-%m-%d'), :white)} — #{parts.join(', ')}"
196
- counts.each { |k, v| total[k] += v }
197
- total_log_mins += log_mins
198
- end
199
-
200
- say set_color("(no data found)", :yellow) unless any
201
-
202
- if dates.size > 1 && any
203
- say set_color("─" * 44, :white)
204
- tparts = []
205
- tparts << "#{total['task']} tasks" if total["task"] > 0
206
- tparts << "#{total['note']} notes" if total["note"] > 0
207
- tparts << "#{total['reminder']} reminders" if total["reminder"] > 0
208
- if total["log"] > 0
209
- h, m = total_log_mins.divmod(60)
210
- dur = total_log_mins > 0 ? " (#{h}h#{m > 0 ? "#{m}m" : ""} total)" : ""
211
- tparts << "#{total['log']} logs#{dur}"
212
- end
213
- say "Total: #{tparts.join(', ')}"
214
- end
215
- rescue StandardError => e
216
- raise Thor::Error, e
217
- end
218
- end
219
-
220
- # ── export ─────────────────────────────────────────────────────────────
221
-
222
- desc "export [DATESIGN]", "Export elements to JSON or CSV (writes to stdout)"
223
- method_option :format, type: :string, default: "json", aliases: "-f",
224
- desc: "Output format: json, csv"
225
- method_option :type, type: :string, aliases: "-t", desc: "Filter by type"
226
- method_option :since, type: :string, aliases: "-S",
227
- desc: "Export from SINCE up to DATESIGN"
228
- def export(datesign = "today")
229
- init
230
- begin
231
- store = ::MPS::Store.new(@config.storage_dir)
232
- date = ::MPS.get_date(datesign)
233
- dates = options[:since] ? date_range(options[:since], date) : [date.to_date]
234
- fmt = options[:format].downcase
235
-
236
- unless %w[json csv].include?(fmt)
237
- raise Thor::Error, "Unknown format '#{fmt}'. Use: json, csv"
238
- end
239
-
240
- records = []
241
- dates.each do |d|
242
- store.parse_date(d).each do |ref, el|
243
- next if el.is_a?(::MPS::Elements::MPS)
244
- next if options[:type] && el.class::SIGNATURE_STAMP != options[:type].downcase
245
- extra = el.parsed_args.reject { |k, _| k == :tags }
246
- records << {
247
- date: d.strftime("%Y-%m-%d"),
248
- ref: ref,
249
- type: el.class::SIGNATURE_STAMP,
250
- tags: el.tags.join(","),
251
- body: el.body_str.strip
252
- }.merge(extra)
253
- end
254
- end
255
-
256
- if fmt == "json"
257
- say JSON.pretty_generate(records)
258
- else
259
- keys = %i[date ref type tags body] +
260
- (records.flat_map(&:keys) - %i[date ref type tags body]).uniq
261
- say CSV.generate { |csv|
262
- csv << keys.map(&:to_s)
263
- records.each { |r| csv << keys.map { |k| r[k] } }
264
- }
265
- end
266
- rescue StandardError => e
267
- raise Thor::Error, e
268
- end
269
- end
270
-
271
- # ── git / autogit / cmd ────────────────────────────────────────────────
272
-
273
- desc "git GITCOMMAND", "Run git commands inside storage_dir"
274
- def git(*commands)
275
- init
276
- begin
277
- git_command = if commands.first == "auto"
278
- "git add . && git commit -m \"$(date)\" && " \
279
- "git pull #{@config.git_remote} #{@config.git_branch} && " \
280
- "git push #{@config.git_remote} #{@config.git_branch}"
281
- elsif commands.first == "autocommit"
282
- "git add . && git commit -m \"$(date)\""
283
- elsif commands.size > 0
284
- cmds = commands.map { |c| c.include?(" ") ? "\"#{c}\"" : c }
285
- "git #{cmds.join(' ')}"
286
- else
287
- "git status"
288
- end
289
- inside(@config.storage_dir) { run git_command }
290
- rescue StandardError => e
291
- raise Thor::Error, e
292
- end
293
- end
294
-
295
- desc "autogit", "Auto stage, commit, pull and push"
296
- def autogit
297
- init
298
- begin
299
- cmd = "git add . && git commit -m \"$(date)\" && " \
300
- "git pull #{@config.git_remote} #{@config.git_branch} && " \
301
- "git push #{@config.git_remote} #{@config.git_branch}"
302
- inside(@config.storage_dir) { run cmd }
303
- rescue StandardError => e
304
- raise Thor::Error, e
305
- end
306
- end
307
-
308
- desc "cmd COMMAND", "Run shell commands inside storage_dir"
309
- def cmd(*commands)
310
- init
311
- begin
312
- cmds = commands.map { |c| c.include?(" ") ? "\"#{c}\"" : c }
313
- inside(@config.storage_dir) { run cmds.join(" ") }
314
- rescue StandardError => e
315
- raise Thor::Error, e
316
- end
317
- end
318
-
319
46
  private
320
47
 
321
48
  # ── config helpers ─────────────────────────────────────────────────────
@@ -340,6 +67,23 @@ module MPS
340
67
  conf_hash
341
68
  end
342
69
 
70
+ # ── store / resolver helpers ───────────────────────────────────────────
71
+
72
+ def store
73
+ @store ||= ::MPS::Store.new(@config.storage_dir)
74
+ end
75
+
76
+ def resolver_for(date)
77
+ ::MPS::RefResolver.new(store.parse_date(date))
78
+ end
79
+
80
+ # ── date helpers ───────────────────────────────────────────────────────
81
+
82
+ def date_range(since_str, to_date)
83
+ since_date = ::MPS.get_date(since_str).to_date
84
+ (since_date..to_date.to_date).to_a
85
+ end
86
+
343
87
  # ── display helpers ────────────────────────────────────────────────────
344
88
 
345
89
  TYPE_COLORS = {
@@ -371,71 +115,82 @@ module MPS
371
115
  end
372
116
  end
373
117
 
374
- def print_element(el, depth: 0)
118
+ # Formats integer minutes as "Xh" or "XhYm".
119
+ def format_duration(minutes)
120
+ return "" unless minutes && minutes > 0
121
+ h, m = minutes.divmod(60)
122
+ m > 0 ? "#{h}h#{m}m" : "#{h}h"
123
+ end
124
+
125
+ def print_element(el, depth: 0, ref: nil)
375
126
  indent = " " * (depth + 1)
376
127
  type_name = el.class::SIGNATURE_STAMP
377
128
  tags_str = el.tags.empty? ? "" : " #{set_color("[#{el.tags.join(', ')}]", :white)}"
378
129
  body_line = el.body_str.strip.lines.first&.strip
379
- say "#{indent}#{type_badge(type_name)} #{element_extra(el)}#{body_line}#{tags_str}"
130
+ ref_col = ref ? "#{set_color(ref.ljust(12), :white)} " : ""
131
+ say "#{indent}#{ref_col}#{type_badge(type_name)} #{element_extra(el)}#{body_line}#{tags_str}"
380
132
  end
381
133
 
382
- # Renders elements_hash as an indented tree ordered by ref path.
383
- # @mps containers show as group headers; the synthetic root wrapper is skipped.
384
- # Returns the count of non-MPS elements actually printed.
385
- def print_tree(elements_hash, opts)
134
+ # Renders elements_hash as indented tree; returns printed count.
135
+ # +header+ is printed once (lazily) before the first visible element, if set.
136
+ def print_tree(elements_hash, opts, resolver: nil, per_line_space: 0, header: nil)
137
+ q = ::MPS::Query.new(opts)
386
138
  sorted = elements_hash.sort_by { |k, _| k.split(".").map(&:to_i) }
387
139
  return 0 if sorted.empty?
388
140
 
389
- root_segs = sorted.first.first.split(".").size # always 1 (just the epoch)
390
- shown = 0
141
+ root_segs = sorted.first.first.split(".").size
142
+ show_refs = opts[:refs]
143
+ shown = 0
144
+ header_shown = false
391
145
 
392
146
  sorted.each do |ref_key, el|
393
147
  depth = ref_key.split(".").size - root_segs - 1
394
- next if depth < 0 # root synthetic @mps wrapper
148
+ next if depth < 0
395
149
 
396
150
  if el.is_a?(::MPS::Elements::MPS)
397
- # Only show @mps group header when it has at least one visible child.
398
151
  prefix = "#{ref_key}."
399
152
  any_visible = elements_hash.any? do |k, v|
400
- k.start_with?(prefix) && !v.is_a?(::MPS::Elements::MPS) && visible?(v, opts)
153
+ k.start_with?(prefix) && !v.is_a?(::MPS::Elements::MPS) && q.match?(v)
401
154
  end
402
155
  next unless any_visible
403
- indent = " " * (depth + 1)
404
- say "#{indent}#{set_color("[@mps]", :white)}"
156
+ unless header_shown
157
+ say set_color("── #{header} ─────────────", :white) if header
158
+ header_shown = true
159
+ end
160
+ indent = " " * (depth + 1)
161
+ human_ref = resolver&.to_human(ref_key) || ref_key
162
+ ref_col = show_refs ? "#{set_color(human_ref.ljust(12), :white)} " : ""
163
+ say "#{indent}#{ref_col}#{set_color("[@mps]", :white)}"
405
164
  else
406
- next unless visible?(el, opts)
407
- print_element(el, depth: depth)
165
+ next unless q.match?(el)
166
+ unless header_shown
167
+ say set_color("── #{header} ─────────────", :white) if header
168
+ header_shown = true
169
+ end
170
+ human_ref = resolver&.to_human(ref_key) || ref_key
171
+ ref_col = show_refs ? human_ref : nil
172
+ print_element(el, depth: depth, ref: ref_col)
173
+ per_line_space.times { say "" }
408
174
  shown += 1
409
175
  end
410
176
  end
411
177
  shown
412
178
  end
413
179
 
414
- def visible?(el, opts)
415
- type_f = opts[:type]&.downcase
416
- tag_f = opts[:tag]
417
- status_f = opts[:status]
418
- return false if type_f && el.class::SIGNATURE_STAMP != type_f
419
- return false if tag_f && !el.tags.include?(tag_f)
420
- # --status only matches elements that carry a status field (i.e. tasks)
421
- if status_f
422
- s = el.respond_to?(:parsed_args) ? el.parsed_args[:status] : nil
423
- return false if s.nil? || s != status_f
424
- end
425
- true
426
- end
180
+ # ── git helpers ────────────────────────────────────────────────────────
427
181
 
428
- # ── filtering (used by search/stats helpers) ────────────────────────────
429
-
430
- def filtered_elements(elements_hash, opts)
431
- elements_hash.values
432
- .reject { |e| e.is_a?(::MPS::Elements::MPS) }
433
- .select { |e| visible?(e, opts) }
182
+ def auto_git_cmd
183
+ "git add . && git commit -m \"$(date)\" && " \
184
+ "git pull #{@config.git_remote} #{@config.git_branch} && " \
185
+ "git push #{@config.git_remote} #{@config.git_branch}"
434
186
  end
435
187
 
436
- def date_range(since_str, to_date)
437
- since_date = ::MPS.get_date(since_str).to_date
438
- (since_date..to_date.to_date).to_a
188
+ # ── type alias resolution ──────────────────────────────────────────────
189
+
190
+ def resolve_type(raw_type)
191
+ normalized = raw_type.downcase
192
+ aliases = (@config.type_aliases || {}).transform_keys(&:to_s)
193
+ aliases.fetch(normalized, normalized)
439
194
  end
440
195
  end
441
196
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ MPS::CLI::MPS.class_eval do
4
+ desc "append TYPE BODY", "Append an element to today's file without opening Vim"
5
+ method_option :tags, type: :string, desc: "Comma-separated tags (e.g. work,release)"
6
+ method_option :status, type: :string, desc: "Task status: open (default) or done"
7
+ method_option :at, type: :string, desc: "Time for reminders (e.g. '3pm')"
8
+ method_option :start_time, type: :string, desc: "Start time for logs (HH:MM)"
9
+ method_option :end_time, type: :string, desc: "End time for logs (HH:MM)"
10
+ def append(type, *body_parts)
11
+ init
12
+ begin
13
+ type = resolve_type(type)
14
+ unless ::MPS::CLI::MPS::VALID_TYPES.include?(type)
15
+ raise Thor::Error, "Unknown type '#{type}'. Valid: #{::MPS::CLI::MPS::VALID_TYPES.join(', ')}"
16
+ end
17
+ body = body_parts.join(" ")
18
+ tags = options[:tags]&.split(",")&.map(&:strip) || []
19
+ attrs = {}
20
+ attrs[:status] = options[:status] if options[:status]
21
+ attrs[:at] = options[:at] if options[:at]
22
+ attrs[:start] = options[:start_time] if options[:start_time]
23
+ attrs[:end] = options[:end_time] if options[:end_time]
24
+
25
+ path = store.append(type: type, body: body, tags: tags, attrs: attrs)
26
+ say_status :appended, "#{type_badge(type)} #{body}", :green
27
+ @config.logger.info("Appended #{type} to #{File.basename(path)}\n")
28
+ rescue StandardError => e
29
+ raise Thor::Error, e
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ MPS::CLI::MPS.class_eval do
4
+ desc "config [SUBCOMMAND]", "Manage MPS configuration (show | edit)"
5
+ def config(subcommand = "show")
6
+ init
7
+ case subcommand.to_s.downcase
8
+ when "show"
9
+ say set_color("MPS configuration", :white)
10
+ say " config file : #{options[:config_path]}"
11
+ say " mps_dir : #{@config.mps_dir}"
12
+ say " storage_dir : #{@config.storage_dir}"
13
+ say " log_file : #{@config.log_file}"
14
+ say " git_remote : #{@config.git_remote}"
15
+ say " git_branch : #{@config.git_branch}"
16
+ say " default_cmd : #{@config.default_command}"
17
+ unless @config.type_aliases.empty?
18
+ say " aliases : #{@config.type_aliases.map { |k, v| "#{k}→#{v}" }.join(', ')}"
19
+ end
20
+ when "edit"
21
+ path = options[:config_path]
22
+ say set_color("Opening #{path} in editor", :white)
23
+ ::TTY::Editor.open(path)
24
+ else
25
+ say set_color("Usage: mps config [show|edit]", :yellow)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ MPS::CLI::MPS.class_eval do
4
+ desc "export [DATESIGN]", "Export elements to JSON or CSV (writes to stdout)"
5
+ method_option :format, type: :string, default: "json", aliases: "-f",
6
+ desc: "Output format: json, csv"
7
+ method_option :type, type: :string, aliases: "-t", desc: "Filter by type"
8
+ method_option :since, type: :string, aliases: "-S",
9
+ desc: "Export from SINCE up to DATESIGN"
10
+ def export(datesign = "today")
11
+ init
12
+ begin
13
+ date = ::MPS.get_date(datesign)
14
+ dates = options[:since] ? date_range(options[:since], date) : [date.to_date]
15
+ fmt = options[:format].downcase
16
+
17
+ unless %w[json csv].include?(fmt)
18
+ raise Thor::Error, "Unknown format '#{fmt}'. Use: json, csv"
19
+ end
20
+
21
+ records = []
22
+ dates.each do |d|
23
+ store.parse_date(d).each do |ref, el|
24
+ next if el.is_a?(::MPS::Elements::MPS) || el.is_a?(::MPS::Engines::Parser::Unknown)
25
+ next if options[:type] && el.class::SIGNATURE_STAMP != options[:type].downcase
26
+ extra = el.parsed_args.reject { |k, _| k == :tags }
27
+ records << {
28
+ date: d.strftime("%Y-%m-%d"),
29
+ ref: ref,
30
+ type: el.class::SIGNATURE_STAMP,
31
+ tags: el.tags.join(","),
32
+ body: el.body_str.strip
33
+ }.merge(extra)
34
+ end
35
+ end
36
+
37
+ if fmt == "json"
38
+ say JSON.pretty_generate(records)
39
+ else
40
+ keys = %i[date ref type tags body] +
41
+ (records.flat_map(&:keys) - %i[date ref type tags body]).uniq
42
+ say CSV.generate { |csv|
43
+ csv << keys.map(&:to_s)
44
+ records.each { |r| csv << keys.map { |k| r[k] } }
45
+ }
46
+ end
47
+ rescue StandardError => e
48
+ raise Thor::Error, e
49
+ end
50
+ end
51
+ end