na 1.2.74 → 1.2.76

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,12 @@
1
+ FROM ruby:3.3.0
2
+ # RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
3
+ RUN mkdir /na
4
+ WORKDIR /na
5
+ RUN gem install bundler:2.2
6
+ COPY ./docker/sources.list /etc/apt/sources.list
7
+ RUN apt-get update -y --allow-insecure-repositories || true
8
+ RUN apt-get install -y sudo || true
9
+ RUN sudo apt-get install -y less vim || true
10
+ COPY ./docker/inputrc /root/.inputrc
11
+ COPY ./docker/bash_profile /root/.bash_profile
12
+ CMD ["/na/scripts/runtests.sh"]
@@ -0,0 +1,17 @@
1
+ #!/bin/bash
2
+ export GLI_DEBUG=true
3
+ export EDITOR="/usr/bin/vim"
4
+ alias b="bundle exec bin/na"
5
+ alias be="bundle exec"
6
+ alias quit="exit"
7
+
8
+ shopt -s nocaseglob
9
+ shopt -s histappend
10
+ shopt -s histreedit
11
+ shopt -s histverify
12
+ shopt -s cmdhist
13
+
14
+ cd /na
15
+ bundle update
16
+ gem update --system
17
+ gem install pkg/*.gem
data/docker/inputrc ADDED
@@ -0,0 +1,57 @@
1
+ "\e[3~": delete-char
2
+ "\ex": 'cd !$ \015ls\015'
3
+ "\ez": 'cd -\015'
4
+ "\e\C-m": '\C-a "$(\C-e|fzf)"\C-a'
5
+ "\e/": '"$(!!|fzf)"\C-a \C-m\C-m'
6
+ # these allow you to use alt+left/right arrow keys
7
+ # to jump the cursor over words
8
+ "\e[1;5C": forward-word
9
+ "\e[1;5D": backward-word
10
+ # "\e[D": backward-word
11
+ # "\e[C": forward-word
12
+ "\ea": menu-complete
13
+ # TAB: menu-complete
14
+ # "\e[Z": "\e-1\C-i"
15
+
16
+ "\e\C-l": history-and-alias-expand-line
17
+
18
+ # these allow you to start typing a command and
19
+ # use the up/down arrow to auto complete from
20
+ # commands in your history
21
+ "\e[B": history-search-forward
22
+ "\e[A": history-search-backward
23
+ "\ew": history-search-backward
24
+ "\es": history-search-forward
25
+ # this lets you hit tab to auto-complete a file or
26
+ # directory name ignoring case
27
+ set completion-ignore-case On
28
+ set mark-symlinked-directories On
29
+ set completion-prefix-display-length 2
30
+ set bell-style none
31
+ # set bell-style visible
32
+ set meta-flag on
33
+ set convert-meta off
34
+ set input-meta on
35
+ set output-meta on
36
+ set show-all-if-ambiguous on
37
+ set show-all-if-unmodified on
38
+ set completion-map-case on
39
+ set visible-stats on
40
+
41
+ # Do history expansion when space entered?
42
+ $if bash
43
+ Space: magic-space
44
+ $endif
45
+
46
+ # Show extra file information when completing, like `ls -F` does
47
+ set visible-stats on
48
+
49
+ # Be more intelligent when autocompleting by also looking at the text after
50
+ # the cursor. For example, when the current line is "cd ~/src/mozil", and
51
+ # the cursor is on the "z", pressing Tab will not autocomplete it to "cd
52
+ # ~/src/mozillail", but to "cd ~/src/mozilla". (This is supported by the
53
+ # Readline used by Bash 4.)
54
+ set skip-completed-text on
55
+
56
+ # Use Alt/Meta + Delete to delete the preceding word
57
+ "\e[3;3~": kill-word
@@ -0,0 +1,11 @@
1
+ deb http://archive.ubuntu.com/ubuntu/ focal main restricted
2
+ deb http://archive.ubuntu.com/ubuntu/ focal-updates main restricted
3
+ deb http://archive.ubuntu.com/ubuntu/ focal universe
4
+ deb http://archive.ubuntu.com/ubuntu/ focal-updates universe
5
+ deb http://archive.ubuntu.com/ubuntu/ focal multiverse
6
+ deb http://archive.ubuntu.com/ubuntu/ focal-updates multiverse
7
+ deb http://archive.ubuntu.com/ubuntu/ focal-backports main restricted universe multiverse
8
+
9
+ deb http://security.ubuntu.com/ubuntu focal-security main restricted
10
+ deb http://security.ubuntu.com/ubuntu focal-security universe
11
+ deb http://security.ubuntu.com/ubuntu focal-security multiversesudo apt update
data/lib/na/actions.rb CHANGED
@@ -11,80 +11,82 @@ module NA
11
11
  ##
12
12
  ## Pretty print a list of actions
13
13
  ##
14
- ## @param actions [Array] The actions
15
- ## @param depth [Number] The depth
16
- ## @param files [Array] The files actions originally came from
17
- ## @param regexes [Array] The regexes used to gather actions
14
+ ## @param depth [Integer] The depth of the action
15
+ ## @param config [Hash] The configuration options
18
16
  ##
19
- def output(depth, files: nil, regexes: [], notes: false, nest: false, nest_projects: false, no_files: false)
20
- return if files.nil?
17
+ ## @option config [Array] :files The files to include in the output
18
+ ## @option config [Array] :regexes The regexes to match against
19
+ ## @option config [Boolean] :notes Whether to include notes in the output
20
+ ## @option config [Boolean] :nest Whether to nest the output
21
+ ## @option config [Boolean] :nest_projects Whether to nest projects in the output
22
+ ## @option config [Boolean] :no_files Whether to include files in the output
23
+ ##
24
+ ## @return [String] The output string
25
+ ##
26
+ def output(depth, config = {})
27
+ defaults = {
28
+ files: nil,
29
+ regexes: [],
30
+ notes: false,
31
+ nest: false,
32
+ nest_projects: false,
33
+ no_files: false,
34
+ }
35
+ config = defaults.merge(config)
21
36
 
22
- if nest
37
+ return if config[:files].nil?
38
+
39
+ if config[:nest]
23
40
  template = NA.theme[:templates][:default]
24
- template = NA.theme[:templates][:no_file] if no_files
41
+ template = NA.theme[:templates][:no_file] if config[:no_files]
25
42
 
26
43
  parent_files = {}
27
44
  out = []
28
45
 
29
- if nest_projects
46
+ if config[:nest_projects]
30
47
  each do |action|
31
- if parent_files.key?(action.file)
32
- parent_files[action.file].push(action)
33
- else
34
- parent_files[action.file] = [action]
35
- end
48
+ parent_files[action.file] ||= []
49
+ parent_files[action.file].push(action)
36
50
  end
37
51
 
38
52
  parent_files.each do |file, acts|
39
53
  projects = NA.project_hierarchy(acts)
40
- out.push("#{file.sub(%r{^./}, '').shorten_path}:")
54
+ out.push("#{file.sub(%r{^./}, "").shorten_path}:")
41
55
  out.concat(NA.output_children(projects, 0))
42
56
  end
43
57
  else
44
58
  template = NA.theme[:templates][:default]
45
- template = NA.theme[:templates][:no_file] if no_files
59
+ template = NA.theme[:templates][:no_file] if config[:no_files]
46
60
 
47
61
  each do |action|
48
- if parent_files.key?(action.file)
49
- parent_files[action.file].push(action)
50
- else
51
- parent_files[action.file] = [action]
52
- end
62
+ parent_files[action.file] ||= []
63
+ parent_files[action.file].push(action)
53
64
  end
54
65
 
55
- parent_files.each do |k, v|
56
- out.push("#{k.sub(%r{^\./}, '')}:")
57
- v.each do |a|
58
- out.push("\t- [#{a.parent.join('/')}] #{a.action}")
66
+ parent_files.each do |file, acts|
67
+ out.push("#{file.sub(%r{^\./}, "")}:")
68
+ acts.each do |a|
69
+ out.push("\t- [#{a.parent.join("/")}] #{a.action}")
59
70
  out.push("\t\t#{a.note.join("\n\t\t")}") unless a.note.empty?
60
71
  end
61
72
  end
62
73
  end
63
74
  NA::Pager.page out.join("\n")
64
75
  else
65
- template =
66
- if no_files
67
- NA.theme[:templates][:no_file]
68
- elsif files.count.positive?
69
- if files.count == 1
70
- NA.theme[:templates][:single_file]
71
- else
72
- NA.theme[:templates][:multi_file]
73
- end
74
- elsif NA.find_files(depth: depth).count > 1
75
- if depth > 1
76
- NA.theme[:templates][:multi_file]
77
- else
78
- NA.theme[:templates][:single_file]
79
- end
80
- else
81
- NA.theme[:templates][:default]
82
- end
83
- template += '%note' if notes
76
+ template = if config[:no_files]
77
+ NA.theme[:templates][:no_file]
78
+ elsif config[:files].count.positive?
79
+ config[:files].count == 1 ? NA.theme[:templates][:single_file] : NA.theme[:templates][:multi_file]
80
+ elsif NA.find_files(depth: depth).count > 1
81
+ depth > 1 ? NA.theme[:templates][:multi_file] : NA.theme[:templates][:single_file]
82
+ else
83
+ NA.theme[:templates][:default]
84
+ end
85
+ template += "%note" if config[:notes]
84
86
 
85
- files.map { |f| NA.notify(f, debug: true) } if files
87
+ config[:files].map { |f| NA.notify(f, debug: true) } if config[:files]
86
88
 
87
- output = map { |action| action.pretty(template: { templates: { output: template } }, regexes: regexes, notes: notes) }
89
+ output = map { |action| action.pretty(template: { templates: { output: template } }, regexes: config[:regexes], notes: config[:notes]) }
88
90
  NA::Pager.page(output.join("\n"))
89
91
  end
90
92
  end
@@ -519,7 +519,7 @@ module NA
519
519
  search: nil,
520
520
  tag: nil
521
521
  }
522
- opts = defaults.merge(options)
522
+ options = defaults.merge(options)
523
523
  files = find_files(depth: options[:depth])
524
524
 
525
525
  files.delete_if do |file|
data/lib/na/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Na
2
- VERSION = '1.2.74'
2
+ VERSION = '1.2.76'
3
3
  end
data/na.gemspec CHANGED
@@ -22,14 +22,13 @@ spec = Gem::Specification.new do |s|
22
22
  s.bindir = 'bin'
23
23
  s.executables << 'na'
24
24
  s.add_development_dependency('minitest', '~> 5.14')
25
- s.add_development_dependency('rake','~> 0.9.2')
26
25
  s.add_development_dependency('rdoc', '~> 4.3')
27
- s.add_development_dependency('rubocop', '~> 1.74')
28
- s.add_development_dependency('yard', '~> 0.9', '>= 0.9.26')
29
26
  s.add_runtime_dependency('chronic', '~> 0.10', '>= 0.10.2')
30
27
  s.add_runtime_dependency('gli','~> 2.21.0')
31
28
  s.add_runtime_dependency('mdless', '~> 1.0', '>= 1.0.32')
32
29
  s.add_runtime_dependency('tty-reader', '~> 0.9', '>= 0.9.0')
33
30
  s.add_runtime_dependency('tty-screen', '~> 0.8', '>= 0.8.1')
34
31
  s.add_runtime_dependency('tty-which', '~> 0.5', '>= 0.5.0')
32
+ s.add_runtime_dependency('git', '~> 3.0.0')
33
+ s.add_development_dependency('tty-spinner', '~> 0.9', '>= 0.9.0')
35
34
  end
@@ -0,0 +1,170 @@
1
+ #!/usr/bin/env ruby
2
+ require 'tty-progressbar'
3
+ require 'shellwords'
4
+
5
+ class ::String
6
+ def short_desc
7
+ split(/[,.]/)[0].sub(/ \(.*?\)?$/, '').strip
8
+ end
9
+
10
+ def ltrunc(max)
11
+ if length > max
12
+ sub(/^.*?(.{#{max - 3}})$/, '...\1')
13
+ else
14
+ self
15
+ end
16
+ end
17
+
18
+ def ltrunc!(max)
19
+ replace ltrunc(max)
20
+ end
21
+ end
22
+
23
+ class FishCompletions
24
+
25
+ attr_accessor :commands, :global_options
26
+
27
+ def generate_helpers
28
+ <<~EOFUNCTIONS
29
+ function __fish_na_needs_command
30
+ # Figure out if the current invocation already has a command.
31
+
32
+ set -l opts a-add add_at= color cwd_as= d-depth= debug ext= f-file= help include_ext n-note p-priority= pager f-recurse t-na_tag= template= version
33
+ set cmd (commandline -opc)
34
+ set -e cmd[1]
35
+ argparse -s $opts -- $cmd 2>/dev/null
36
+ or return 0
37
+ # These flags function as commands, effectively.
38
+ if set -q argv[1]
39
+ # Also print the command, so this can be used to figure out what it is.
40
+ echo $argv[1]
41
+ return 1
42
+ end
43
+ return 0
44
+ end
45
+
46
+ function __fish_na_using_command
47
+ set -l cmd (__fish_na_needs_command)
48
+ test -z "$cmd"
49
+ and return 1
50
+ contains -- $cmd $argv
51
+ and return 0
52
+ end
53
+
54
+ function __fish_na_subcommands
55
+ na help -c
56
+ end
57
+
58
+ complete -c na -f
59
+ complete -xc na -n '__fish_na_needs_command' -a '(__fish_na_subcommands)'
60
+
61
+ complete -xc na -n '__fish_seen_subcommand_from help; and not __fish_seen_subcommand_from (na help -c)' -a "(na help -c)"
62
+ EOFUNCTIONS
63
+ end
64
+
65
+ def get_help_sections(command = '')
66
+ res = `na help #{command}`.strip
67
+ scanned = res.scan(/(?m-i)^([A-Z ]+)\n([\s\S]*?)(?=\n+[A-Z]+|\Z)/)
68
+ sections = {}
69
+ scanned.each do |sect|
70
+ title = sect[0].downcase.strip.gsub(/ +/, '_').to_sym
71
+ content = sect[1].split(/\n/).map(&:strip).delete_if(&:empty?)
72
+ sections[title] = content
73
+ end
74
+ sections
75
+ end
76
+
77
+ def parse_option(option)
78
+ res = option.match(/(?:-(?<short>\w), )?(?:--(?:\[no-\])?(?<long>w+)(?:=(?<arg>\w+))?)\s+- (?<desc>.*?)$/)
79
+ return nil unless res
80
+ {
81
+ short: res['short'],
82
+ long: res['long'],
83
+ arg: res[:arg],
84
+ description: res['desc'].short_desc
85
+ }
86
+ end
87
+
88
+ def parse_options(options)
89
+ options.map { |opt| parse_option(opt) }
90
+ end
91
+
92
+ def parse_command(command)
93
+ res = command.match(/^(?<cmd>[^, \t]+)(?<alias>(?:, [^, \t]+)*)?\s+- (?<desc>.*?)$/)
94
+ commands = [res['cmd']]
95
+ commands.concat(res['alias'].split(/, /).delete_if(&:empty?)) if res['alias']
96
+
97
+ {
98
+ commands: commands,
99
+ description: res['desc'].short_desc
100
+ }
101
+ end
102
+
103
+ def parse_commands(commands)
104
+ commands.map { |cmd| parse_command(cmd) }
105
+ end
106
+
107
+ def generate_subcommand_completions
108
+ out = []
109
+ @commands.each_with_index do |cmd, i|
110
+ out << "complete -xc na -n '__fish_na_needs_command' -a '#{cmd[:commands].join(' ')}' -d #{Shellwords.escape(cmd[:description])}"
111
+ end
112
+
113
+ out.join("\n")
114
+ end
115
+
116
+ def generate_subcommand_option_completions
117
+
118
+ out = []
119
+ need_export = []
120
+
121
+ @commands.each_with_index do |cmd, i|
122
+ @bar.advance
123
+ data = get_help_sections(cmd[:commands].first)
124
+
125
+ if data[:synopsis].join(' ').strip.split(/ /).last =~ /(path|file)/i
126
+ out << "complete -c na -F -n '__fish_na_using_command #{cmd[:commands].join(" ")}'"
127
+ end
128
+
129
+ if data[:command_options]
130
+ parse_options(data[:command_options]).each do |option|
131
+ next if option.nil?
132
+
133
+ arg = option[:arg] ? '-r' : ''
134
+ short = option[:short] ? "-s #{option[:short]}" : ''
135
+ long = option[:long] ? "-l #{option[:long]}" : ''
136
+ out << "complete -c na #{long} #{short} -f #{arg} -n '__fish_na_using_command #{cmd[:commands].join(' ')}' -d #{Shellwords.escape(option[:description])}"
137
+
138
+ need_export.concat(cmd[:commands]) if option[:long] == 'output'
139
+ end
140
+ end
141
+ end
142
+
143
+ unless need_export.empty?
144
+ out << "complete -f -c na -s o -l output -x -n '__fish_na_using_command #{need_export.join(' ')}' -a '(__fish_na_export_plugins)'"
145
+ end
146
+
147
+ # clear
148
+ out.join("\n")
149
+ end
150
+
151
+ def initialize
152
+ data = get_help_sections
153
+ @global_options = parse_options(data[:global_options])
154
+ @commands = parse_commands(data[:commands])
155
+ @bar = TTY::ProgressBar.new("\033[0;0;33mGenerating Fish completions: \033[0;35;40m[:bar]\033[0m", total: @commands.count, bar_format: :blade)
156
+ @bar.resize(25)
157
+ end
158
+
159
+ def generate_completions
160
+ @bar.start
161
+ out = []
162
+ out << generate_helpers
163
+ out << generate_subcommand_completions
164
+ out << generate_subcommand_option_completions
165
+ @bar.finish
166
+ out.join("\n")
167
+ end
168
+ end
169
+
170
+ puts FishCompletions.new.generate_completions
@@ -0,0 +1,5 @@
1
+ #!/bin/bash
2
+
3
+ cd /planter
4
+ bundle update
5
+ rake test
data/src/_README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  _If you're one of the rare people like me who find this useful, feel free to
10
10
  [buy me some coffee][donate]._
11
11
 
12
- The current version of `na` is <!--VER-->1.2.73<!--END VER-->.
12
+ The current version of `na` is <!--VER-->1.2.75<!--END VER-->.
13
13
 
14
14
  `na` ("next action") is a command line tool designed to make it easy to see what your next actions are for any project, right from the command line. It works with TaskPaper-formatted files (but any plain text format will do), looking for `@na` tags (or whatever you specify) in todo files in your current folder.
15
15
 
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: na
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.74
4
+ version: 1.2.76
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brett Terpstra
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-03-16 00:00:00.000000000 Z
10
+ date: 2025-05-10 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: minitest
@@ -23,20 +23,6 @@ dependencies:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
25
  version: '5.14'
26
- - !ruby/object:Gem::Dependency
27
- name: rake
28
- requirement: !ruby/object:Gem::Requirement
29
- requirements:
30
- - - "~>"
31
- - !ruby/object:Gem::Version
32
- version: 0.9.2
33
- type: :development
34
- prerelease: false
35
- version_requirements: !ruby/object:Gem::Requirement
36
- requirements:
37
- - - "~>"
38
- - !ruby/object:Gem::Version
39
- version: 0.9.2
40
26
  - !ruby/object:Gem::Dependency
41
27
  name: rdoc
42
28
  requirement: !ruby/object:Gem::Requirement
@@ -51,40 +37,6 @@ dependencies:
51
37
  - - "~>"
52
38
  - !ruby/object:Gem::Version
53
39
  version: '4.3'
54
- - !ruby/object:Gem::Dependency
55
- name: rubocop
56
- requirement: !ruby/object:Gem::Requirement
57
- requirements:
58
- - - "~>"
59
- - !ruby/object:Gem::Version
60
- version: '1.74'
61
- type: :development
62
- prerelease: false
63
- version_requirements: !ruby/object:Gem::Requirement
64
- requirements:
65
- - - "~>"
66
- - !ruby/object:Gem::Version
67
- version: '1.74'
68
- - !ruby/object:Gem::Dependency
69
- name: yard
70
- requirement: !ruby/object:Gem::Requirement
71
- requirements:
72
- - - "~>"
73
- - !ruby/object:Gem::Version
74
- version: '0.9'
75
- - - ">="
76
- - !ruby/object:Gem::Version
77
- version: 0.9.26
78
- type: :development
79
- prerelease: false
80
- version_requirements: !ruby/object:Gem::Requirement
81
- requirements:
82
- - - "~>"
83
- - !ruby/object:Gem::Version
84
- version: '0.9'
85
- - - ">="
86
- - !ruby/object:Gem::Version
87
- version: 0.9.26
88
40
  - !ruby/object:Gem::Dependency
89
41
  name: chronic
90
42
  requirement: !ruby/object:Gem::Requirement
@@ -199,6 +151,40 @@ dependencies:
199
151
  - - ">="
200
152
  - !ruby/object:Gem::Version
201
153
  version: 0.5.0
154
+ - !ruby/object:Gem::Dependency
155
+ name: git
156
+ requirement: !ruby/object:Gem::Requirement
157
+ requirements:
158
+ - - "~>"
159
+ - !ruby/object:Gem::Version
160
+ version: 3.0.0
161
+ type: :runtime
162
+ prerelease: false
163
+ version_requirements: !ruby/object:Gem::Requirement
164
+ requirements:
165
+ - - "~>"
166
+ - !ruby/object:Gem::Version
167
+ version: 3.0.0
168
+ - !ruby/object:Gem::Dependency
169
+ name: tty-spinner
170
+ requirement: !ruby/object:Gem::Requirement
171
+ requirements:
172
+ - - "~>"
173
+ - !ruby/object:Gem::Version
174
+ version: '0.9'
175
+ - - ">="
176
+ - !ruby/object:Gem::Version
177
+ version: 0.9.0
178
+ type: :development
179
+ prerelease: false
180
+ version_requirements: !ruby/object:Gem::Requirement
181
+ requirements:
182
+ - - "~>"
183
+ - !ruby/object:Gem::Version
184
+ version: '0.9'
185
+ - - ">="
186
+ - !ruby/object:Gem::Version
187
+ version: 0.9.0
202
188
  description: A tool for managing a TaskPaper file of project todos for the current
203
189
  directory. Easily create "next actions" to come back to, add tags and priorities,
204
190
  and notes. Add prompt hooks to display your next actions automatically when cd'ing
@@ -211,6 +197,8 @@ extra_rdoc_files:
211
197
  - README.md
212
198
  - na.rdoc
213
199
  files:
200
+ - ".rubocop.yml"
201
+ - ".rubocop_todo.yml"
214
202
  - ".travis.yml"
215
203
  - CHANGELOG.md
216
204
  - Gemfile
@@ -241,6 +229,14 @@ files:
241
229
  - bin/commands/undo.rb
242
230
  - bin/commands/update.rb
243
231
  - bin/na
232
+ - docker/Dockerfile
233
+ - docker/Dockerfile-2.6
234
+ - docker/Dockerfile-2.7
235
+ - docker/Dockerfile-3.0
236
+ - docker/Dockerfile-3.3
237
+ - docker/bash_profile
238
+ - docker/inputrc
239
+ - docker/sources.list
244
240
  - lib/na.rb
245
241
  - lib/na/action.rb
246
242
  - lib/na/actions.rb
@@ -260,6 +256,8 @@ files:
260
256
  - na.gemspec
261
257
  - na.rdoc
262
258
  - scripts/fixreadme.rb
259
+ - scripts/generate-fish-completions.rb
260
+ - scripts/runtests.sh
263
261
  - src/_README.md
264
262
  - test.md
265
263
  - test2.txt
@@ -288,7 +286,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
288
286
  - !ruby/object:Gem::Version
289
287
  version: '0'
290
288
  requirements: []
291
- rubygems_version: 3.6.5
289
+ rubygems_version: 3.6.6
292
290
  specification_version: 4
293
291
  summary: A command line tool for adding and listing project todos
294
292
  test_files: []