trak_flow 0.1.3

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.
Files changed (95) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc +3 -0
  3. data/CHANGELOG.md +69 -0
  4. data/COMMITS.md +196 -0
  5. data/Gemfile +8 -0
  6. data/Gemfile.lock +281 -0
  7. data/README.md +479 -0
  8. data/Rakefile +16 -0
  9. data/bin/tf +6 -0
  10. data/bin/tf_mcp +81 -0
  11. data/docs/.keep +0 -0
  12. data/docs/api/database.md +434 -0
  13. data/docs/api/ruby-library.md +349 -0
  14. data/docs/api/task-model.md +341 -0
  15. data/docs/assets/stylesheets/extra.css +53 -0
  16. data/docs/assets/trak_flow.jpg +0 -0
  17. data/docs/cli/admin-commands.md +369 -0
  18. data/docs/cli/dependency-commands.md +321 -0
  19. data/docs/cli/label-commands.md +222 -0
  20. data/docs/cli/overview.md +163 -0
  21. data/docs/cli/plan-commands.md +344 -0
  22. data/docs/cli/task-commands.md +333 -0
  23. data/docs/core-concepts/dependencies.md +232 -0
  24. data/docs/core-concepts/labels.md +217 -0
  25. data/docs/core-concepts/overview.md +178 -0
  26. data/docs/core-concepts/plans-workflows.md +264 -0
  27. data/docs/core-concepts/tasks.md +205 -0
  28. data/docs/getting-started/configuration.md +120 -0
  29. data/docs/getting-started/installation.md +79 -0
  30. data/docs/getting-started/quick-start.md +245 -0
  31. data/docs/index.md +169 -0
  32. data/docs/mcp/integration.md +302 -0
  33. data/docs/mcp/overview.md +206 -0
  34. data/docs/mcp/resources.md +284 -0
  35. data/docs/mcp/tools.md +457 -0
  36. data/examples/basic_usage.rb +365 -0
  37. data/examples/cli_demo.sh +314 -0
  38. data/examples/mcp/Gemfile +9 -0
  39. data/examples/mcp/Gemfile.lock +226 -0
  40. data/examples/mcp/http_demo.rb +232 -0
  41. data/examples/mcp/stdio_demo.rb +146 -0
  42. data/lib/trak_flow/cli/admin_commands.rb +136 -0
  43. data/lib/trak_flow/cli/config_commands.rb +260 -0
  44. data/lib/trak_flow/cli/dep_commands.rb +71 -0
  45. data/lib/trak_flow/cli/label_commands.rb +76 -0
  46. data/lib/trak_flow/cli/main_commands.rb +386 -0
  47. data/lib/trak_flow/cli/plan_commands.rb +185 -0
  48. data/lib/trak_flow/cli/workflow_commands.rb +133 -0
  49. data/lib/trak_flow/cli.rb +110 -0
  50. data/lib/trak_flow/config/defaults.yml +114 -0
  51. data/lib/trak_flow/config/section.rb +74 -0
  52. data/lib/trak_flow/config.rb +276 -0
  53. data/lib/trak_flow/graph/dependency_graph.rb +288 -0
  54. data/lib/trak_flow/id_generator.rb +52 -0
  55. data/lib/trak_flow/mcp/resources/base_resource.rb +25 -0
  56. data/lib/trak_flow/mcp/resources/dependency_graph.rb +31 -0
  57. data/lib/trak_flow/mcp/resources/label_list.rb +21 -0
  58. data/lib/trak_flow/mcp/resources/plan_by_id.rb +27 -0
  59. data/lib/trak_flow/mcp/resources/plan_list.rb +21 -0
  60. data/lib/trak_flow/mcp/resources/task_by_id.rb +31 -0
  61. data/lib/trak_flow/mcp/resources/task_list.rb +21 -0
  62. data/lib/trak_flow/mcp/resources/task_next.rb +30 -0
  63. data/lib/trak_flow/mcp/resources/workflow_by_id.rb +27 -0
  64. data/lib/trak_flow/mcp/resources/workflow_list.rb +21 -0
  65. data/lib/trak_flow/mcp/server.rb +140 -0
  66. data/lib/trak_flow/mcp/tools/base_tool.rb +29 -0
  67. data/lib/trak_flow/mcp/tools/comment_add.rb +33 -0
  68. data/lib/trak_flow/mcp/tools/dep_add.rb +34 -0
  69. data/lib/trak_flow/mcp/tools/dep_remove.rb +25 -0
  70. data/lib/trak_flow/mcp/tools/label_add.rb +28 -0
  71. data/lib/trak_flow/mcp/tools/label_remove.rb +25 -0
  72. data/lib/trak_flow/mcp/tools/plan_add_step.rb +35 -0
  73. data/lib/trak_flow/mcp/tools/plan_create.rb +33 -0
  74. data/lib/trak_flow/mcp/tools/plan_run.rb +58 -0
  75. data/lib/trak_flow/mcp/tools/plan_start.rb +58 -0
  76. data/lib/trak_flow/mcp/tools/task_block.rb +27 -0
  77. data/lib/trak_flow/mcp/tools/task_close.rb +26 -0
  78. data/lib/trak_flow/mcp/tools/task_create.rb +51 -0
  79. data/lib/trak_flow/mcp/tools/task_defer.rb +27 -0
  80. data/lib/trak_flow/mcp/tools/task_start.rb +25 -0
  81. data/lib/trak_flow/mcp/tools/task_update.rb +36 -0
  82. data/lib/trak_flow/mcp/tools/workflow_discard.rb +28 -0
  83. data/lib/trak_flow/mcp/tools/workflow_summarize.rb +34 -0
  84. data/lib/trak_flow/mcp.rb +38 -0
  85. data/lib/trak_flow/models/comment.rb +71 -0
  86. data/lib/trak_flow/models/dependency.rb +96 -0
  87. data/lib/trak_flow/models/label.rb +90 -0
  88. data/lib/trak_flow/models/task.rb +188 -0
  89. data/lib/trak_flow/storage/database.rb +638 -0
  90. data/lib/trak_flow/storage/jsonl.rb +259 -0
  91. data/lib/trak_flow/time_parser.rb +15 -0
  92. data/lib/trak_flow/version.rb +5 -0
  93. data/lib/trak_flow.rb +100 -0
  94. data/mkdocs.yml +143 -0
  95. metadata +392 -0
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TrakFlow
4
+ class CLI < Thor
5
+ # Admin subcommands
6
+ class AdminCommands < Thor
7
+ class_option :json, aliases: "-j", type: :boolean, default: false, desc: "Output in JSON format"
8
+
9
+ desc "cleanup", "Clean up old closed tasks"
10
+ option :older_than, type: :numeric, default: 30, desc: "Days since closed"
11
+ option :dry_run, type: :boolean, default: false, desc: "Show what would be deleted"
12
+ option :force, type: :boolean, default: false, desc: "Skip confirmation"
13
+ option :cascade, type: :boolean, default: false, desc: "Also delete children"
14
+ def cleanup
15
+ with_database do |db|
16
+ cutoff = Time.now.utc - (options[:older_than] * 24 * 60 * 60)
17
+ candidates = db.list_tasks(status: "closed", include_tombstones: true)
18
+ .select { |i| i.closed_at && i.closed_at < cutoff }
19
+
20
+ if candidates.empty?
21
+ puts "No tasks to clean up"
22
+ return
23
+ end
24
+
25
+ if options[:dry_run]
26
+ puts "Would delete #{candidates.size} task(s):"
27
+ candidates.each { |i| puts " #{i.id}: #{i.title}" }
28
+ return
29
+ end
30
+
31
+ unless options[:force]
32
+ puts "About to delete #{candidates.size} task(s). Continue? (y/n)"
33
+ return unless $stdin.gets.strip.downcase == "y"
34
+ end
35
+
36
+ candidates.each do |task|
37
+ db.child_tasks(task.id).each { |c| db.delete_task(c.id) } if options[:cascade]
38
+ db.delete_task(task.id)
39
+ end
40
+
41
+ output({ deleted: candidates.size }) do
42
+ puts "Deleted #{candidates.size} task(s)"
43
+ end
44
+ end
45
+ end
46
+
47
+ desc "compact", "Compact the database"
48
+ option :analyze, type: :boolean, default: false, desc: "Show compaction stats"
49
+ option :apply, type: :boolean, default: false, desc: "Apply compaction"
50
+ def compact
51
+ with_database do |db|
52
+ stats = {
53
+ total_tasks: db.all_task_ids.size,
54
+ closed_tasks: db.list_tasks(status: "closed", include_tombstones: true).size,
55
+ ephemeral: db.find_ephemeral_workflows.size,
56
+ plans: db.find_plans.size,
57
+ workflows: db.find_workflows.size
58
+ }
59
+
60
+ if options[:analyze]
61
+ output(stats) do
62
+ stats.each { |k, v| puts "#{k}: #{v}" }
63
+ end
64
+ return
65
+ end
66
+
67
+ if options[:apply]
68
+ db.list_tasks(status: "closed").each do |task|
69
+ next unless task.closed_at && task.closed_at < (Time.now.utc - 30 * 24 * 60 * 60)
70
+
71
+ task.status = "tombstone"
72
+ db.update_task(task)
73
+ end
74
+ puts "Compaction complete"
75
+ else
76
+ puts "Use --analyze to see stats or --apply to compact"
77
+ end
78
+ end
79
+ end
80
+
81
+ desc "graph", "Generate dependency graph"
82
+ option :format, type: :string, default: "dot", desc: "Output format (dot, svg)"
83
+ option :output, aliases: "-o", type: :string, desc: "Output file"
84
+ option :include_closed, type: :boolean, default: false, desc: "Include closed tasks"
85
+ def graph
86
+ with_database do |db|
87
+ dep_graph = Graph::DependencyGraph.new(db)
88
+
89
+ graph_output = case options[:format]
90
+ when "svg" then dep_graph.to_svg(include_closed: options[:include_closed])
91
+ else dep_graph.to_dot(include_closed: options[:include_closed])
92
+ end
93
+
94
+ if options[:output]
95
+ File.write(options[:output], graph_output)
96
+ puts "Graph written to #{options[:output]}"
97
+ else
98
+ puts graph_output
99
+ end
100
+ end
101
+ end
102
+
103
+ desc "analyze", "Analyze the task graph"
104
+ def analyze
105
+ with_database do |db|
106
+ dep_graph = Graph::DependencyGraph.new(db)
107
+ analysis = dep_graph.analyze
108
+
109
+ output(analysis) do
110
+ analysis.each do |k, v|
111
+ if v.is_a?(Array)
112
+ puts "#{k}:"
113
+ v.each { |item| puts " - #{item}" }
114
+ else
115
+ puts "#{k}: #{v}"
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
121
+
122
+ private
123
+
124
+ # Delegate helper methods to parent CLI
125
+ def with_database(&block) = CLI.new.with_database(&block)
126
+
127
+ def output(json_data, &human_block)
128
+ if options[:json]
129
+ puts Oj.dump(json_data, mode: :compat, indent: 2)
130
+ else
131
+ human_block.call
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,260 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TrakFlow
4
+ class CLI < Thor
5
+ # Config subcommands
6
+ class ConfigCommands < Thor
7
+ class_option :json, aliases: "-j", type: :boolean, default: false, desc: "Output in JSON format"
8
+
9
+ XDG_CONFIG_PATH = File.expand_path("~/.config/trak_flow/trak_flow.yml").freeze
10
+ PROJECT_CONFIG_PATH = ".trak_flow/config.yml"
11
+
12
+ desc "show", "Show the current configuration file"
13
+ def show
14
+ active_path = find_active_config_path
15
+
16
+ if active_path.nil?
17
+ output({ error: "No config file found", paths: { xdg: XDG_CONFIG_PATH, project: PROJECT_CONFIG_PATH } }) do
18
+ puts pastel.yellow("No configuration file found.")
19
+ puts "Create one with: tf config reset"
20
+ puts ""
21
+ puts "Expected locations:"
22
+ puts " XDG: #{XDG_CONFIG_PATH}"
23
+ puts " Project: #{File.expand_path(PROJECT_CONFIG_PATH)}"
24
+ end
25
+ return
26
+ end
27
+
28
+ config_content = File.read(active_path)
29
+
30
+ if options[:json]
31
+ yaml_data = YAML.safe_load(config_content, permitted_classes: [Symbol], symbolize_names: true)
32
+ puts Oj.dump({ path: active_path, config: yaml_data }, mode: :compat, indent: 2)
33
+ else
34
+ puts pastel.bold("# #{active_path}")
35
+ puts config_content
36
+ end
37
+ end
38
+
39
+ desc "defaults", "Show the bundled default configuration"
40
+ def defaults
41
+ defaults_content = File.read(Config::DEFAULTS_PATH)
42
+
43
+ if options[:json]
44
+ yaml_data = YAML.safe_load(defaults_content, permitted_classes: [Symbol], symbolize_names: true)
45
+ puts Oj.dump(yaml_data, mode: :compat, indent: 2)
46
+ else
47
+ puts pastel.bold("# TrakFlow Bundled Defaults")
48
+ puts pastel.dim("# #{Config::DEFAULTS_PATH}")
49
+ puts ""
50
+ puts defaults_content
51
+ end
52
+ end
53
+
54
+ # Show defaults when no subcommand is given
55
+ default_task :defaults
56
+
57
+ desc "reset", "Reset configuration to defaults"
58
+ option :global, aliases: "-g", type: :boolean, default: false, desc: "Reset global (XDG) config"
59
+ option :force, aliases: "-f", type: :boolean, default: false, desc: "Overwrite existing config"
60
+ def reset
61
+ target_path = determine_config_path(options[:global])
62
+ target_dir = File.dirname(target_path)
63
+
64
+ if File.exist?(target_path) && !options[:force]
65
+ output({ success: false, error: "Config file already exists. Use --force to overwrite." }) do
66
+ puts pastel.red("Config file already exists at #{target_path}")
67
+ puts "Use --force to overwrite"
68
+ end
69
+ return
70
+ end
71
+
72
+ FileUtils.mkdir_p(target_dir)
73
+ FileUtils.cp(Config::DEFAULTS_PATH, target_path)
74
+
75
+ output({ success: true, path: target_path }) do
76
+ puts pastel.green("Configuration reset to defaults at #{target_path}")
77
+ end
78
+ end
79
+
80
+ desc "get KEY", "Get a configuration value (e.g., 'database.path', 'mcp.port')"
81
+ def get(key)
82
+ value = get_nested_value(key)
83
+
84
+ if value.nil?
85
+ output({ key: key, value: nil, error: "Key not found" }) do
86
+ puts pastel.red("Key '#{key}' not found in configuration")
87
+ end
88
+ return
89
+ end
90
+
91
+ output({ key: key, value: serialize_value(value) }) do
92
+ if value.is_a?(TrakFlow::ConfigSection)
93
+ puts value.to_h.to_yaml.lines[1..].join
94
+ else
95
+ puts value
96
+ end
97
+ end
98
+ end
99
+
100
+ desc "set KEY VALUE", "Set a configuration value (e.g., 'database.path /path/to/db')"
101
+ def set(key, value)
102
+ config_path = find_writable_config_path
103
+
104
+ # Load existing config or create new
105
+ config_data = if File.exist?(config_path)
106
+ YAML.safe_load(File.read(config_path), permitted_classes: [Symbol], symbolize_names: true) || {}
107
+ else
108
+ {}
109
+ end
110
+
111
+ # Ensure defaults section exists
112
+ config_data[:defaults] ||= {}
113
+
114
+ # Parse the value (convert to appropriate type)
115
+ parsed_value = parse_value(value)
116
+
117
+ # Set the nested value
118
+ set_nested_value(config_data[:defaults], key, parsed_value)
119
+
120
+ # Ensure directory exists
121
+ FileUtils.mkdir_p(File.dirname(config_path))
122
+
123
+ # Write the config file
124
+ File.write(config_path, config_data.to_yaml)
125
+
126
+ # Reset config to pick up new values
127
+ TrakFlow.reset_config!
128
+
129
+ output({ key: key, value: parsed_value, path: config_path }) do
130
+ puts pastel.green("Set #{key} = #{parsed_value}")
131
+ puts "Configuration saved to #{config_path}"
132
+ end
133
+ end
134
+
135
+ desc "path", "Show configuration file paths"
136
+ def path
137
+ project_path = File.expand_path(PROJECT_CONFIG_PATH)
138
+ project_exists = File.exist?(project_path)
139
+ xdg_exists = File.exist?(XDG_CONFIG_PATH)
140
+
141
+ paths = {
142
+ defaults: Config::DEFAULTS_PATH,
143
+ xdg: { path: XDG_CONFIG_PATH, exists: xdg_exists },
144
+ project: { path: project_path, exists: project_exists },
145
+ active: find_active_config_path
146
+ }
147
+
148
+ output(paths) do
149
+ puts "Configuration paths:"
150
+ puts " Defaults: #{Config::DEFAULTS_PATH}"
151
+ puts " XDG: #{XDG_CONFIG_PATH} #{xdg_exists ? pastel.green('(exists)') : pastel.dim('(not found)')}"
152
+ puts " Project: #{project_path} #{project_exists ? pastel.green('(exists)') : pastel.dim('(not found)')}"
153
+ puts ""
154
+ puts "Active config: #{find_active_config_path || pastel.dim('(defaults only)')}"
155
+ end
156
+ end
157
+
158
+ private
159
+
160
+ def pastel
161
+ @pastel ||= Pastel.new
162
+ end
163
+
164
+ def output(json_data, &human_block)
165
+ if options[:json]
166
+ puts Oj.dump(json_data, mode: :compat, indent: 2)
167
+ else
168
+ human_block.call
169
+ end
170
+ end
171
+
172
+ def determine_config_path(global)
173
+ if global
174
+ XDG_CONFIG_PATH
175
+ elsif File.exist?(PROJECT_CONFIG_PATH)
176
+ File.expand_path(PROJECT_CONFIG_PATH)
177
+ elsif File.directory?(".trak_flow")
178
+ File.expand_path(PROJECT_CONFIG_PATH)
179
+ else
180
+ XDG_CONFIG_PATH
181
+ end
182
+ end
183
+
184
+ def find_writable_config_path
185
+ # Prefer project config if .trak_flow exists, otherwise use XDG
186
+ if File.directory?(".trak_flow")
187
+ File.expand_path(PROJECT_CONFIG_PATH)
188
+ else
189
+ XDG_CONFIG_PATH
190
+ end
191
+ end
192
+
193
+ def find_active_config_path
194
+ project_path = File.expand_path(PROJECT_CONFIG_PATH)
195
+ return project_path if File.exist?(project_path)
196
+ return XDG_CONFIG_PATH if File.exist?(XDG_CONFIG_PATH)
197
+
198
+ nil
199
+ end
200
+
201
+ def get_nested_value(key)
202
+ parts = key.split(".")
203
+ value = TrakFlow.config
204
+
205
+ parts.each do |part|
206
+ if value.respond_to?(part)
207
+ value = value.send(part)
208
+ elsif value.respond_to?(:[])
209
+ value = value[part.to_sym] || value[part]
210
+ else
211
+ return nil
212
+ end
213
+ return nil if value.nil?
214
+ end
215
+
216
+ value
217
+ end
218
+
219
+ def set_nested_value(hash, key, value)
220
+ parts = key.split(".")
221
+ current = hash
222
+
223
+ parts[0...-1].each do |part|
224
+ current[part.to_sym] ||= {}
225
+ current = current[part.to_sym]
226
+ end
227
+
228
+ current[parts.last.to_sym] = value
229
+ end
230
+
231
+ def parse_value(value)
232
+ # Try to parse as various types
233
+ case value.downcase
234
+ when "true" then true
235
+ when "false" then false
236
+ when "nil", "null" then nil
237
+ else
238
+ # Try integer
239
+ if value.match?(/\A-?\d+\z/)
240
+ value.to_i
241
+ # Try float
242
+ elsif value.match?(/\A-?\d+\.\d+\z/)
243
+ value.to_f
244
+ else
245
+ # Keep as string, expand ~ for paths
246
+ value.start_with?("~") ? File.expand_path(value) : value
247
+ end
248
+ end
249
+ end
250
+
251
+ def serialize_value(value)
252
+ if value.is_a?(TrakFlow::ConfigSection)
253
+ value.to_h
254
+ else
255
+ value
256
+ end
257
+ end
258
+ end
259
+ end
260
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TrakFlow
4
+ class CLI < Thor
5
+ # Dependency subcommands
6
+ class DepCommands < Thor
7
+ class_option :json, aliases: "-j", type: :boolean, default: false, desc: "Output in JSON format"
8
+
9
+ desc "add SOURCE TARGET", "Add a dependency"
10
+ option :type, aliases: "-t", type: :string, default: "blocks", desc: "Dependency type"
11
+ def add(source, target)
12
+ with_database do |db|
13
+ dep = Models::Dependency.new(
14
+ source_id: source,
15
+ target_id: target,
16
+ type: options[:type]
17
+ )
18
+ db.add_dependency(dep)
19
+
20
+ output(dep.to_h) do
21
+ puts "Added dependency: #{source} #{options[:type]} #{target}"
22
+ end
23
+ end
24
+ end
25
+
26
+ desc "remove SOURCE TARGET", "Remove a dependency"
27
+ option :type, aliases: "-t", type: :string, desc: "Dependency type"
28
+ def remove(source, target)
29
+ with_database do |db|
30
+ count = db.remove_dependency(source, target, type: options[:type])
31
+
32
+ output({ removed: count }) do
33
+ puts "Removed #{count} dependency(ies)"
34
+ end
35
+ end
36
+ end
37
+
38
+ desc "tree ID", "Show dependency tree"
39
+ def tree(id)
40
+ with_database do |db|
41
+ graph = Graph::DependencyGraph.new(db)
42
+ tree_data = graph.dependency_tree(id)
43
+
44
+ output(tree_data) do
45
+ print_tree(tree_data, 0)
46
+ end
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def print_tree(node, depth)
53
+ indent = " " * depth
54
+ puts "#{indent}#{status_icon(node[:status])} #{node[:id]}: #{node[:title]}"
55
+ node[:children].each { |child| print_tree(child, depth + 1) }
56
+ end
57
+
58
+ # Delegate helper methods to parent CLI
59
+ def with_database(&block) = CLI.new.with_database(&block)
60
+ def status_icon(status) = CLI.new.status_icon(status)
61
+
62
+ def output(json_data, &human_block)
63
+ if options[:json]
64
+ puts Oj.dump(json_data, mode: :compat, indent: 2)
65
+ else
66
+ human_block.call
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TrakFlow
4
+ class CLI < Thor
5
+ # Label subcommands
6
+ class LabelCommands < Thor
7
+ class_option :json, aliases: "-j", type: :boolean, default: false, desc: "Output in JSON format"
8
+
9
+ desc "add ID LABEL", "Add a label to a task"
10
+ def add(id, label_name)
11
+ with_database do |db|
12
+ label = Models::Label.new(task_id: id, name: label_name)
13
+ db.add_label(label)
14
+
15
+ output(label.to_h) do
16
+ puts "Added label '#{label_name}' to #{id}"
17
+ end
18
+ end
19
+ end
20
+
21
+ desc "remove ID LABEL", "Remove a label from a task"
22
+ def remove(id, label_name)
23
+ with_database do |db|
24
+ count = db.remove_label(id, label_name)
25
+
26
+ output({ removed: count }) do
27
+ puts count.positive? ? "Removed label '#{label_name}' from #{id}" : "Label not found"
28
+ end
29
+ end
30
+ end
31
+
32
+ desc "list ID", "List labels for a task"
33
+ def list(id)
34
+ with_database do |db|
35
+ labels = db.find_labels(id)
36
+
37
+ output(labels.map(&:to_h)) do
38
+ if labels.empty?
39
+ puts "No labels"
40
+ else
41
+ labels.each { |l| puts l.name }
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ desc "list-all", "List all labels in the database"
48
+ def list_all
49
+ with_database do |db|
50
+ labels = db.all_labels
51
+
52
+ output(labels) do
53
+ if labels.empty?
54
+ puts "No labels"
55
+ else
56
+ labels.each { |l| puts l }
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ # Delegate helper methods to parent CLI
65
+ def with_database(&block) = CLI.new.with_database(&block)
66
+
67
+ def output(json_data, &human_block)
68
+ if options[:json]
69
+ puts Oj.dump(json_data, mode: :compat, indent: 2)
70
+ else
71
+ human_block.call
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end