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.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +23 -18
- data/.gitignore +4 -3
- data/ARCHITECTURE.md +298 -0
- data/CLAUDE.md +6 -10
- data/GETTING_STARTED.md +176 -8
- data/Gemfile.lock +46 -0
- data/Rakefile +6 -5
- data/lib/cli/mps.rb +88 -333
- data/lib/mps/cli/commands/append.rb +32 -0
- data/lib/mps/cli/commands/config_cmd.rb +28 -0
- data/lib/mps/cli/commands/export.rb +51 -0
- data/lib/mps/cli/commands/git.rb +44 -0
- data/lib/mps/cli/commands/list.rb +51 -0
- data/lib/mps/cli/commands/open.rb +25 -0
- data/lib/mps/cli/commands/search.rb +34 -0
- data/lib/mps/cli/commands/stats.rb +65 -0
- data/lib/mps/cli/commands/tags.rb +33 -0
- data/lib/mps/cli/commands/update.rb +54 -0
- data/lib/mps/cli/commands.rb +5 -0
- data/lib/mps/config.rb +8 -3
- data/lib/mps/constants.rb +7 -5
- data/lib/mps/elements/element.rb +41 -11
- data/lib/mps/elements/elements.rb +8 -6
- data/lib/mps/elements/log.rb +2 -4
- data/lib/mps/elements/mps.rb +1 -4
- data/lib/mps/elements/note.rb +1 -4
- data/lib/mps/elements/reminder.rb +1 -4
- data/lib/mps/elements/task.rb +1 -4
- data/lib/mps/engines/engines.rb +3 -1
- data/lib/mps/engines/mps.rb +20 -10
- data/lib/mps/interpolators/interpolators.rb +3 -1
- data/lib/mps/mps.rb +9 -19
- data/lib/mps/presenter.rb +128 -0
- data/lib/mps/query.rb +71 -0
- data/lib/mps/ref_resolver.rb +76 -0
- data/lib/mps/store.rb +95 -6
- data/lib/mps/version.rb +1 -1
- data/lib/mps.rb +11 -9
- data/mps.gemspec +15 -24
- data/prompt.txt +64 -0
- metadata +28 -90
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(:
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
4
|
-
require
|
|
5
|
-
require
|
|
6
|
-
require
|
|
7
|
-
require
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
383
|
-
#
|
|
384
|
-
|
|
385
|
-
|
|
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
|
|
390
|
-
|
|
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
|
|
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) &&
|
|
153
|
+
k.start_with?(prefix) && !v.is_a?(::MPS::Elements::MPS) && q.match?(v)
|
|
401
154
|
end
|
|
402
155
|
next unless any_visible
|
|
403
|
-
|
|
404
|
-
|
|
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
|
|
407
|
-
|
|
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
|
-
|
|
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
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|