├── bin ├── t └── check ├── examples └── hello.txt ├── lib ├── checkcheckit │ ├── version.rb │ ├── list.rb │ └── console.rb └── checkcheckit.rb ├── .travis.yml ├── Rakefile ├── Gemfile ├── .gitignore ├── test ├── any_file_test.rb ├── list_test.rb ├── helper.rb ├── resume_test.rb ├── start_test.rb └── console_test.rb ├── checkcheckit.gemspec ├── LICENSE.txt └── README.md /bin/t: -------------------------------------------------------------------------------- 1 | bundle exec turn -Itest test/*_test.rb 2 | -------------------------------------------------------------------------------- /examples/hello.txt: -------------------------------------------------------------------------------- 1 | - say hello 2 | `echo hello` 3 | -------------------------------------------------------------------------------- /lib/checkcheckit/version.rb: -------------------------------------------------------------------------------- 1 | module CheckCheckIt 2 | VERSION = "0.5.0" 3 | end 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 1.9.3 4 | - jruby-19mode # JRuby in 1.9 mode 5 | - rbx-19mode 6 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require 'vault-test-tools/rake_task' 3 | 4 | task(default: :test) 5 | -------------------------------------------------------------------------------- /bin/check: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'checkcheckit' 4 | 5 | CheckCheckIt::Console.new.run! ARGV 6 | -------------------------------------------------------------------------------- /lib/checkcheckit.rb: -------------------------------------------------------------------------------- 1 | require 'lucy-goosey' 2 | require 'excon' 3 | require 'yajl/json_gem' 4 | 5 | require "checkcheckit/version" 6 | 7 | module CheckCheckIt 8 | end 9 | 10 | require "checkcheckit/console" 11 | require 'checkcheckit/list' 12 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in checkcheckit.gemspec 4 | gemspec 5 | 6 | group :test do 7 | gem 'rake' 8 | gem 'fakefs' 9 | gem 'ruby-debug19' 10 | gem 'vault-test-tools', '~> 0.2.2' 11 | gem 'rr' 12 | end 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | bin/minitar 19 | bin/rdebug 20 | bin/turn 21 | test/.test.profile* 22 | vendor 23 | -------------------------------------------------------------------------------- /test/any_file_test.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | class AnyFileTest < CheckCheckIt::TestCase 4 | 5 | def setup 6 | super 7 | dir = File.join('/just/another') 8 | FileUtils.mkdir_p(dir) 9 | File.open(File.join(dir, 'file'), 'w') do |file| 10 | file << "- 1\n" 11 | file << "- 2\n- 3\n" 12 | end 13 | console.in_stream = MiniTest::Mock.new 14 | console.web_socket = MiniTest::Mock.new 15 | end 16 | 17 | def test_with_any_file 18 | 3.times { console.in_stream.expect :gets, "y" } 19 | result = check "start /just/another/file" 20 | console.in_stream.verify 21 | end 22 | 23 | end 24 | -------------------------------------------------------------------------------- /test/list_test.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | class ListTest < CheckCheckIt::TestCase 4 | 5 | def setup 6 | super 7 | Examples.create_grocery_list(home) 8 | @list = List.new('~/checkcheckit/personal/groceries') 9 | end 10 | 11 | def test_list_parses_steps 12 | @list.steps.size.must_equal 3 13 | end 14 | 15 | 16 | def test_to_json 17 | @list.to_h.must_equal({ 18 | name: 'groceries', 19 | steps: [ 20 | { name: 'pineapple', body: '' }, 21 | { name: 'mangoes', body: " enhance the flavor with \n spice\n" }, 22 | { name: 'fudge', body: ' best from a place in sutter creek'} 23 | ] 24 | }) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /checkcheckit.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'checkcheckit/version' 5 | 6 | Gem::Specification.new do |gem| 7 | gem.name = "checkcheckit" 8 | gem.version = CheckCheckIt::VERSION 9 | gem.authors = ["Chris Continanza"] 10 | gem.email = ["christopher.continanza@gmail.com"] 11 | gem.description = %q{Checklists like a boss} 12 | gem.summary = %q{Command line tool for using checklists} 13 | gem.homepage = "" 14 | 15 | gem.files = `git ls-files`.split($/) 16 | gem.executables = 'check' 17 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 18 | gem.require_paths = ["lib"] 19 | 20 | gem.add_dependency 'lucy-goosey', '~> 0.2.0' 21 | gem.add_dependency 'excon' 22 | gem.add_dependency 'yajl-ruby' 23 | gem.add_dependency 'colored' 24 | end 25 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Chris Continanza 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /lib/checkcheckit/list.rb: -------------------------------------------------------------------------------- 1 | class List 2 | attr_accessor :name, :body, :steps 3 | 4 | def initialize(file) 5 | fname = File.basename(file) 6 | @name = fname.sub(File.extname(fname), '') 7 | @body = File.read(file) 8 | @steps = parse_steps(@body) 9 | @current_step = 0 10 | end 11 | 12 | def header 13 | return if @body.lines.to_a.empty? 14 | if line = @body.lines.first.strip 15 | line if line =~ /#/ 16 | end 17 | end 18 | 19 | def to_h 20 | {name: @name, steps: @steps.map(&:to_h)} 21 | end 22 | 23 | private 24 | 25 | def parse_steps(body) 26 | steps = [] 27 | current_step = nil 28 | body.lines.each do |line| 29 | next if line.strip.empty? 30 | if line =~ /^-/ 31 | current_name = line.sub(/^-/,'').strip 32 | current_step = Step.new(current_name) 33 | steps << current_step 34 | elsif current_step 35 | current_step.body << line 36 | end 37 | end 38 | steps 39 | end 40 | 41 | class Step 42 | attr_accessor :name, :body, :commands 43 | 44 | def initialize(name, body = '') 45 | @name = name 46 | @body = body 47 | end 48 | 49 | def commands 50 | @body.scan(/`([^`]+)`/).map do |match,_| 51 | match 52 | end 53 | end 54 | 55 | def to_h 56 | {name: @name, body: @body} 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.expand_path(File.join(File.dirname(__FILE__), '../lib'))) 2 | ENV['RACK_ENV'] = 'test' 3 | ENV['CHECKCHECKIT_URL'] = 'http://example.com' 4 | 5 | require 'vault-test-tools' 6 | require 'minitest/mock' 7 | require 'fakefs' 8 | require 'checkcheckit' 9 | require 'rr' 10 | 11 | module Examples 12 | def self.create_grocery_list(home) 13 | dir = File.join(home, 'personal') 14 | FileUtils.mkdir_p(dir) 15 | File.open(File.join(dir, 'groceries'), 'w') do |file| 16 | file << "# These be the groceries\n" 17 | file << "- pineapple \n" 18 | file << "- mangoes \n enhance the flavor with \n spice\n" 19 | file << "- fudge \n best from a place in sutter creek" 20 | end 21 | end 22 | end 23 | 24 | module ConsoleTestHelpers 25 | # Internal: Runs command lines in tests like their command line invocation. 26 | # 27 | # Examples: 28 | # 29 | # # check 'list' 30 | # 31 | # # check 'list deploy' 32 | # 33 | # # check 'start deploy' 34 | def check(cmd_string) 35 | console.run! cmd_string.split(' ') 36 | end 37 | 38 | def reset_console 39 | ## Clear out the output 40 | @console = nil 41 | end 42 | 43 | def console 44 | @console ||= CheckCheckIt::Console.new(out_stream: StringIO.new) 45 | end 46 | 47 | def output 48 | console.out_stream.string 49 | end 50 | 51 | def home 52 | console.list_dir || '~/checkcheckit' 53 | end 54 | 55 | def setup 56 | super 57 | reset_console 58 | FakeFS.activate! 59 | Excon.defaults[:mock] = true 60 | end 61 | 62 | def teardown 63 | super 64 | FakeFS.deactivate! 65 | Excon.stubs.clear 66 | end 67 | end 68 | 69 | CheckCheckIt::TestCase = Class.new(Vault::TestCase) 70 | CheckCheckIt::Spec = Class.new(Vault::Spec) 71 | MiniTest::Spec.register_spec_type //, CheckCheckIt::Spec 72 | Vault::Test.include_in_all Vault::Test::EnvironmentHelpers, ConsoleTestHelpers, RR::Adapters::TestUnit 73 | 74 | -------------------------------------------------------------------------------- /test/resume_test.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe 'Resuming a checklist' do 4 | def setup 5 | super 6 | Examples.create_grocery_list(home) 7 | end 8 | 9 | #TODO: verify though the console 10 | 11 | it "should record the pass/fail of each step" do 12 | console.in_stream = MiniTest::Mock.new 13 | console.out_stream = MiniTest::Mock.new 14 | 15 | 9.times do 16 | console.out_stream.expect :puts, true, [String] 17 | end 18 | 3.times do 19 | console.out_stream.expect :print, true, ["Check: "] 20 | end 21 | console.in_stream.expect :gets, "n" 22 | console.in_stream.expect :gets, "y" 23 | console.in_stream.expect :gets, "n" 24 | result = check "start groceries" 25 | console.in_stream.verify 26 | console.out_stream.verify 27 | result["results"][0][:result].must_equal "FAIL" 28 | result["results"][0][:status].must_equal 0 29 | result["results"][1][:result].must_equal "CHECK" 30 | result["results"][1][:status].must_equal 1 31 | result["results"][2][:result].must_equal "FAIL" 32 | result["results"][2][:status].must_equal 0 33 | end 34 | 35 | it "should record the name of each step" do 36 | console.in_stream = MiniTest::Mock.new 37 | 6.times { console.in_stream.expect :gets, "y" } 38 | result = check "start groceries" 39 | console.in_stream.verify 40 | result["results"][0][:name].must_equal "pineapple" 41 | result["results"][1][:name].must_equal "mangoes" 42 | result["results"][2][:name].must_equal "fudge" 43 | end 44 | 45 | it "should record the body of each step" do 46 | console.in_stream = MiniTest::Mock.new 47 | 6.times { console.in_stream.expect :gets, "y" } 48 | result = check "start groceries" 49 | console.in_stream.verify 50 | =begin 51 | result["results"][0][:body].must_equal "" 52 | result["results"][1][:name].must_equal "enhance the flavor with \n spice" 53 | result["results"][2][:name].must_equal "best from a place in sutter creek" 54 | =end 55 | end 56 | end 57 | 58 | 59 | -------------------------------------------------------------------------------- /test/start_test.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | class StartTest < CheckCheckIt::TestCase 4 | 5 | def setup 6 | super 7 | Examples.create_grocery_list(home) 8 | console.in_stream = MiniTest::Mock.new 9 | end 10 | 11 | def test_list_parses_steps 12 | 3.times { console.in_stream.expect :gets, "y" } 13 | result = check "start groceries" 14 | console.in_stream.verify 15 | end 16 | 17 | def test_proper_output_on_no_args 18 | check "start" 19 | output.must_include "No list given" 20 | output.must_include "groceries" 21 | end 22 | 23 | # When you do 24 | # 25 | # check start deploy --live 26 | # 27 | # we setup the POST with the list data 28 | def test_live_flag_triggers_live_mode 29 | #stub setup 30 | Excon.stub({:method => :post, :body => { 31 | emails: nil, 32 | list: List.new(home + '/personal/groceries').to_h 33 | }.to_json}, {:status => 200,:body => '"DEADBEEF"'}) 34 | 35 | #stub checks 36 | 3.times do |i| 37 | Excon.stub({:method => :post, :path => "/DEADBEEF/check/#{i}" }, 38 | {:status => 200}) 39 | end 40 | 3.times { console.in_stream.expect :gets, "y" } 41 | check "start groceries --live" 42 | end 43 | 44 | # When you do 45 | # 46 | # check start deploy --email csquared@heroku.com 47 | # 48 | # The webservice sends you an email with a link to the list so 49 | # you can run it on the web. 50 | def test_email_flag_triggers_live_mode 51 | # stub setup 52 | Excon.stub({:method => :post, :body => { 53 | emails: "csquared@heroku.com,thea.lamkin@gmail.com", 54 | list: List.new(home + '/personal/groceries').to_h 55 | }.to_json}, {:status => 200, :body => '"DEADBEEF"'}) 56 | 57 | # stub checks 58 | 3.times do |i| 59 | Excon.stub({:method => :post, :path => "/DEADBEEF/check/#{i}" }, 60 | {:status => 200}) 61 | end 62 | 3.times { console.in_stream.expect :gets, "y" } 63 | check "start groceries --email csquared@heroku.com,thea.lamkin@gmail.com" 64 | end 65 | 66 | # Same as above but with env set 67 | def test_reads_email_from_env 68 | set_env 'CHECKCHECKIT_EMAIL', "csquared@heroku.com,thea.lamkin@gmail.com" 69 | 70 | #stub setup 71 | Excon.stub({:method => :post, :body => { 72 | emails: "csquared@heroku.com,thea.lamkin@gmail.com", 73 | list: List.new(home + '/personal/groceries').to_h 74 | }.to_json}, {:status => 200, :body => '"FOO"'}) 75 | 76 | # stub checks 77 | 3.times do |i| 78 | Excon.stub({:method => :post, :path => "/FOO/check/#{i}" }, 79 | {:status => 200}) 80 | end 81 | 82 | 3.times { console.in_stream.expect :gets, "y" } 83 | check "start groceries" 84 | end 85 | 86 | def test_live_only_sends_on_success 87 | #stub setup 88 | Excon.stub({:method => :post, :body => { 89 | emails: nil, 90 | list: List.new(home + '/personal/groceries').to_h 91 | }.to_json}, {:status => 200, :body => '"FOO"'}) 92 | 93 | # stub checks 94 | Excon.stub({:method => :post, :path => "/FOO/check/2" }, 95 | {:status => 200}) 96 | 97 | 2.times { console.in_stream.expect :gets, "n" } 98 | 1.times { console.in_stream.expect :gets, "y" } 99 | check "start groceries --live" 100 | end 101 | 102 | =begin 103 | def test_reads_url_from_env 104 | #skip "too lazy to implement" 105 | end 106 | =end 107 | end 108 | 109 | -------------------------------------------------------------------------------- /test/console_test.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | class ConsoleTest < CheckCheckIt::TestCase 4 | def setup 5 | super 6 | end 7 | 8 | def test_lists_orgs_and_lists_with_headers 9 | Examples.create_grocery_list(home) 10 | check "list" 11 | assert_match(/^# Checklists\n/, output) 12 | assert_match(/^personal\n/, output) 13 | assert_match(/^ groceries\t # These be the groceries\n/, output) 14 | end 15 | 16 | def test_configurable_list_dir 17 | dir = File.join('/foo', 'personal') 18 | FileUtils.mkdir_p(dir) 19 | 20 | check "list --home /foo" 21 | assert_match(/^personal\n/, output) 22 | end 23 | 24 | 25 | def test_default_no_notes 26 | Examples.create_grocery_list(home) 27 | console.in_stream = MiniTest::Mock.new 28 | console.out_stream = MiniTest::Mock.new 29 | 30 | 9.times do 31 | console.out_stream.expect :puts, true, [String] 32 | end 33 | 3.times do 34 | console.out_stream.expect :print, true, ["Check: "] 35 | end 36 | console.in_stream.expect :gets, "n" 37 | console.in_stream.expect :gets, "y" 38 | console.in_stream.expect :gets, "n" 39 | result = check "start groceries" 40 | end 41 | 42 | def test_runs_embedded_commands 43 | # setup 44 | dir = File.join(home, 'personal') 45 | FileUtils.mkdir_p(dir) 46 | File.open(File.join(dir, 'commands'), 'w') do |file| 47 | file << "- just one command \n `git pull`" 48 | end 49 | console.in_stream = MiniTest::Mock.new 50 | console.out_stream = MiniTest::Mock.new 51 | 52 | # console iteraction 53 | console.out_stream.expect :puts, true, ["|.| Step 1: #{'just one command'.bold}"] 54 | console.out_stream.expect :puts, true, [" `git pull`"] 55 | console.out_stream.expect :puts, true, ["\nRun command `#{'git pull'.white}`?"] 56 | console.out_stream.expect :print, true, [",y,n: "] 57 | console.in_stream.expect :gets, "" 58 | console.out_stream.expect :puts, true, ["running...".green] 59 | mock(console).system("git pull") { true } 60 | console.out_stream.expect :print, true, ["Check: "] 61 | console.in_stream.expect :gets, "" 62 | console.out_stream.expect :puts, true, [""] 63 | console.out_stream.expect :puts, true, ["|#{'+'.green}| Done"] 64 | 65 | #assert 66 | check "start commands" 67 | console.in_stream.verify 68 | console.out_stream.verify 69 | end 70 | 71 | # so you can use --notes and save notes with each step 72 | # *we actually don't use this that much* 73 | def test_includes_notes 74 | Examples.create_grocery_list(home) 75 | console.in_stream = MiniTest::Mock.new 76 | console.out_stream = MiniTest::Mock.new 77 | 78 | #check 1 79 | console.out_stream.expect :puts, true, ["|...| Step 1: #{'pineapple'.bold}"] 80 | console.out_stream.expect :print, true, ["Check: "] 81 | console.in_stream.expect :gets, "n" 82 | 83 | # notes 1 84 | console.out_stream.expect :print, true, ["Notes: "] 85 | console.in_stream.expect :gets, "Shit's fucked" 86 | console.out_stream.expect :puts, true, [''] 87 | 88 | #check 2 89 | progress = '|' + '-'.red + '..|' 90 | console.out_stream.expect :puts, true, ["#{progress} Step 2: #{'mangoes'.bold}"] 91 | console.out_stream.expect :puts, true, [String] 92 | console.out_stream.expect :print, true, ["Check: "] 93 | console.in_stream.expect :gets, "y" 94 | 95 | # notes 2 96 | console.out_stream.expect :print, true, ["Notes: "] 97 | console.in_stream.expect :gets, "Tasty" 98 | console.out_stream.expect :puts, true, [''] 99 | 100 | # NO FUDGE FOR YOU 101 | progress = '|' + '-'.red + '+'.green + '.|' 102 | console.out_stream.expect :puts, true, ["#{progress} Step 3: #{'fudge'.bold}"] 103 | console.out_stream.expect :puts, true, [String] 104 | console.out_stream.expect :print, true, ["Check: "] 105 | console.in_stream.expect :gets, "n" 106 | 107 | # notes 3 108 | console.out_stream.expect :print, true, ["Notes: "] 109 | console.in_stream.expect :gets, "Tasty" 110 | console.out_stream.expect :puts, true, [''] 111 | 112 | progress = '|' + '-'.red + '+'.green + '-'.red + '|' 113 | console.out_stream.expect :puts, true, ["#{progress} Done"] 114 | 115 | check "start groceries --notes" 116 | console.in_stream.verify 117 | console.out_stream.verify 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /lib/checkcheckit/console.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | require 'uri' 3 | require 'colored' 4 | 5 | # Uses the "big bowl of pudding' architecture 6 | class CheckCheckIt::Console 7 | DEFAULT_URL = 'http://checkcheckit.herokuapp.com/' 8 | attr_accessor :list_dir 9 | attr_accessor :out_stream, :in_stream, :web_socket 10 | 11 | def initialize(opts = {}) 12 | @out_stream = opts[:out_stream] || $stdout 13 | @in_stream = opts[:in_stream] || $stdin 14 | end 15 | 16 | def dir 17 | File.expand_path(@list_dir) 18 | end 19 | 20 | def run!(args = []) 21 | @options = Lucy::Goosey.parse_options(args) 22 | @options['email'] ||= ENV['CHECKCHECKIT_EMAIL'] 23 | @list_dir = File.expand_path(@options.fetch('home', '~/checkcheckit')) 24 | 25 | if args.length == 0 26 | puts "No command given".red 27 | else 28 | method = args.shift 29 | if respond_to? method 30 | send method, args 31 | else 32 | puts "did not understand: #{method}" 33 | end 34 | end 35 | end 36 | 37 | def debug? 38 | @options['d'] || @options['debug'] 39 | end 40 | 41 | def start(args) 42 | target = args.first 43 | unless target 44 | puts "No list given.\n\n" 45 | list(args) 46 | return 47 | end 48 | 49 | expanded_target = File.expand_path(target) 50 | list_name = nil 51 | 52 | # see if its a Path 53 | if File.exists?(expanded_target) 54 | list_name = expanded_target 55 | else 56 | #finding the list 57 | list_name = Dir[dir + '/*/*'].find{ |fname| fname.include? target } 58 | end 59 | 60 | if list_name 61 | list = List.new(list_name) 62 | if (emails = @options['email']) || @options['live'] 63 | @list_id = list_id = notify_server_of_start(emails, list) 64 | $stderr.puts web_service_url, list_id if debug? 65 | url = URI.join(web_service_url, list_id) 66 | puts "Live at URL: #{url}" 67 | 68 | if @options['open'] || @options['O'] 69 | Process.detach fork{ exec("open #{url}") } 70 | end 71 | 72 | return if @options['no-cli'] || @options['web-only'] 73 | 74 | @live = true 75 | end 76 | 77 | step_through_list(list) 78 | else 79 | puts "Could not find checklist via: #{target}" 80 | end 81 | end 82 | 83 | def list(args) 84 | puts "# Checklists\n" 85 | Dir[dir + '/*'].each do |dir| 86 | top_level_dir = File.basename dir 87 | puts top_level_dir 88 | Dir[dir + '/*'].each do |file| 89 | list = List.new(file) 90 | puts " #{list.name}\t #{list.header}" 91 | end 92 | end 93 | end 94 | 95 | private 96 | def step_through_list(list) 97 | results = Array.new(list.steps.length, false) 98 | 99 | list.steps.each_with_index do |step, i| 100 | header = "Step #{i+1}" 101 | puts "#{fmt_results(results)} #{header}: #{step.name.bold}" 102 | puts step.body unless step.body.empty? 103 | 104 | check, notes = nil 105 | begin 106 | step.commands.each do |command| 107 | puts "\nRun command `#{command.white}`?" 108 | print ",y,n: " 109 | input = in_stream.gets.chomp 110 | puts input.inspect if debug? 111 | case input 112 | when /^(y|)$/ 113 | puts "running...".green 114 | system(command) 115 | else 116 | puts "skipping".red 117 | end 118 | end 119 | 120 | print "Check: " 121 | case input = in_stream.gets 122 | when /^[y|+]$/ || '' 123 | check = true 124 | when /^[n|-]$/ 125 | check = false 126 | else 127 | check = true 128 | end 129 | 130 | if @options['notes'] || @options['n'] 131 | print "Notes: " 132 | notes = in_stream.gets 133 | end 134 | rescue Interrupt => e 135 | puts "\nGoodbye!" 136 | return 137 | end 138 | 139 | if check && @live 140 | update_server_with_step(i) 141 | end 142 | 143 | results[i] = { 144 | step: i + 1, 145 | name: step.name, 146 | body: step.body, 147 | check: check, 148 | result: check ? 'CHECK' : 'FAIL', 149 | status: check ? 1 : 0, 150 | notes: notes 151 | } 152 | 153 | puts 154 | end 155 | 156 | puts "#{fmt_results(results)} Done" 157 | save_results(list, results) 158 | end 159 | 160 | def save_results(list,results) 161 | report = { 162 | 'list-name' => list.name, 163 | 'results' => results 164 | } 165 | end 166 | 167 | def fmt_results(results) 168 | keys = results.map do |result| 169 | if result 170 | result[:check] ? '+'.green : '-'.red 171 | else 172 | '.' 173 | end 174 | end 175 | "|#{keys.join}|" 176 | end 177 | 178 | def puts(text = '') 179 | @out_stream.puts text 180 | end 181 | 182 | def print(text = '') 183 | @out_stream.print text 184 | end 185 | 186 | def web_service_url 187 | ENV['CHECKCHECKIT_URL'] || DEFAULT_URL 188 | end 189 | 190 | # Returns id 191 | def notify_server_of_start(emails, list) 192 | begin 193 | response = Excon.post(web_service_url, :body => { 194 | emails: emails, 195 | list: list.to_h 196 | }.to_json, 197 | :headers => { 198 | 'Content-Type' => 'application/json' 199 | }) 200 | $stderr.puts response if debug? 201 | return response.body.gsub('"','') 202 | rescue Excon::Errors::SocketError => e 203 | puts "Error connecting to #{web_service_url}" 204 | end 205 | end 206 | 207 | def update_server_with_step(step_id) 208 | begin 209 | url = URI.join(web_service_url, "/#{@list_id}/check/#{step_id}").to_s 210 | Excon.post(url) 211 | rescue Excon::Errors::SocketError, ArgumentError => e 212 | puts "Error POSTing to #{url}" 213 | end 214 | end 215 | end 216 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Check, Check, It 2 | 3 | checkcheckit is a ruby gem that provides a `check` binary that lets you run 4 | through simple, text-based checklists with a step-by-step prompt. 5 | 6 | You also get a web-based interface that lets you collaborate on your checklist 7 | progress with friends or coworkers. 8 | 9 | ## Installation 10 | 11 | $ gem install checkcheckit 12 | 13 | ## Usage 14 | 15 | A checklist is just a text file that supports a subset of Markdown formatting. 16 | Every line that starts with a `-` is a step. 17 | Everything beneath a step is that step's body or description. 18 | 19 | Here's an example checklist: 20 | 21 | $ cat sandwich.md 22 | - Buy the ingredients. 23 | Make sure the bread is fresh and soft, and the peanut butter should be 24 | organic for best results. 25 | - Apply the peanut butter. 26 | Do this evenly on both slices of bread. 27 | - Apply the jelly. 28 | Use 1-2oz, measured with a home scale. 29 | - Close sandwich. 30 | - Eat. 31 | 32 | When you want to make a sandwich, start the checklist with `check start`: 33 | 34 | $ check start sandwich.md 35 | |.....| Step 1: Buy the ingredients. 36 | Make sure the bread is fresh and soft, and the peanut butter should be 37 | organic for best results. 38 | Check: 39 | 40 | |+....| Step 2: Apply the peanut butter. 41 | Do this evenly on both slices of bread. 42 | Check: 43 | 44 | |++...| Step 3: Apply the jelly. 45 | Use 1-2oz, measured with a home scale. 46 | Check: 47 | 48 | |+++..| Step 4: Close sandwich. 49 | Check: 50 | 51 | |++++.| Step 5: Eat. 52 | Check: 53 | 54 | |+++++| Done 55 | 56 | You can now make a sandwich the same way every time. 57 | 58 | ## Advanced usage 59 | 60 | ### Shell out to commands 61 | 62 | `check` will recognize any text that is surrounded with backticks: 63 | \`command with args\` as a command to potentially run. 64 | 65 | It will prompt you if you'd like to run the command. You will then have the 66 | option to check it off if the output looks correct to you. 67 | 68 | For example: 69 | 70 | $ cat hello.txt 71 | - say hello 72 | `echo hello` 73 | 74 | $ check start hello.txt 75 | |.| Step 1: say hello 76 | `echo hello` 77 | 78 | Run command `echo hello`? 79 | ,y,n: 80 | running `echo hello` 81 | hello 82 | Check: 83 | 84 | |+| Done 85 | 86 | ### Centralized checklists 87 | 88 | You can put all of your checklists into `~/checkcheckit`, and start them with 89 | shorthand form. If there are multiple checklists with the same name use the 90 | format `folder/checklist`. 91 | 92 | List your checklists 93 | 94 | $ check list 95 | # Checklists 96 | personal 97 | groceries 98 | work 99 | deploy 100 | 101 | Start with shortcut names 102 | 103 | $ check start groceries 104 | 105 | One great use case is to create a git repository of checklists for your team. 106 | 107 | $ mkdir -p ~/src/team/checklists 108 | $ cd ~/src/team/checklists 109 | $ git init 110 | $ vim deploy.md && git commit -a -m "first checklist" 111 | $ mkdir -p ~/checkcheckit 112 | $ ln -s ~/src/team/checklists ~/checkcheckit/team 113 | $ check list 114 | # Checklists 115 | team 116 | deploy # Deploy the app 117 | 118 | ### Options 119 | 120 | #### `--live` mode 121 | 122 | This is fun. 123 | 124 | `check start --live` will create an _interactive_ companion URL on the web. 125 | 126 | This URL is websockets-enabled and communicates with the command line. 127 | This means command line 'checks' get pushed to the web. Once a list is on the web you can 128 | disconnect the command line and continue finishing it (with others). 129 | 130 | $ check start deploy --live 131 | Live at URL: http://checkcheckit.herokuapp.com/4f24b9d933d5467ec913461b8da3f952dbe724cb 132 | Websocket refused connection - using POST 133 | |........| Step 1: Make sure there are no uncommitted changes 134 | > `git status` 135 | Check: 136 | 137 | |+.......| Step 2: Pull everything from git 138 | > `git pull` 139 | Check: ^C 140 | bye 141 | 142 | During that console session the web UI would be interactively crossing items off the list: 143 | 144 | 145 | #### `--web-only`, `--open`, `--no-cli` 146 | 147 | Start a list, open it in your browser, and skip the CLI interaction 148 | 149 | $ check start groceries --live --web-only --open 150 | $ check start groceries --live --no-cli -O 151 | Live at URL: http://checkcheckit.herokuapp.com/4f24b9d933d5467ec913461b8da3f952dbe724cb 152 | 153 | Run commands from the checklist 154 | 155 | $ cat ./hello.txt 156 | - say hello 157 | `echo hello` 158 | 159 | $ check start ./hello.txt 160 | |.| Step 1: say hello 161 | `echo hello` 162 | 163 | Run command `echo hello`? 164 | ,y,n: 165 | running `echo hello` 166 | hello 167 | Check: 168 | 169 | |+| Done 170 | 171 | When you iterate through a checklist you can just type "enter", "y", or "+" to confirm a step and "no" or "-" to 172 | fail one. 173 | 174 | #### `--open/-O` 175 | 176 | `check start --live -O/--open` will open the url in your browser by shelling out to `open` 177 | 178 | #### `--email ` 179 | Specify an email (or a comma-separated list) on the command line via the `--email` flag and 180 | the address(es) will receive an email with a link to a web version of the checklist. 181 | 182 | 183 | $ check start deploy --email bob@work.com,steve@work.com 184 | Live at URL: http://checkcheckit.herokuapp.com/4f24b9d933d5467ec913461b8da3f952dbe724cb 185 | Websocket refused connection - using POST 186 | |........| Step 1: Make sure there are no uncommitted changes 187 | > `git status` 188 | Check: ^C 189 | bye 190 | 191 | ## TODO 192 | 193 | - resume a run locally from URL 194 | - push notes to web 195 | - emit pass/fail and colorize 196 | - post to campfire 197 | 198 | ## Contributing 199 | 200 | 1. Fork it 201 | 2. Create your feature branch (`git checkout -b my-new-feature`) 202 | 3. Commit your changes (`git commit -am 'Add some feature'`) 203 | 4. Push to the branch (`git push origin my-new-feature`) 204 | 5. Create new Pull Request 205 | --------------------------------------------------------------------------------