kimurai 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +1923 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/kimurai +6 -0
- data/kimurai.gemspec +48 -0
- data/lib/kimurai.rb +53 -0
- data/lib/kimurai/automation/deploy.yml +54 -0
- data/lib/kimurai/automation/setup.yml +44 -0
- data/lib/kimurai/automation/setup/chromium_chromedriver.yml +26 -0
- data/lib/kimurai/automation/setup/firefox_geckodriver.yml +20 -0
- data/lib/kimurai/automation/setup/phantomjs.yml +33 -0
- data/lib/kimurai/automation/setup/ruby_environment.yml +124 -0
- data/lib/kimurai/base.rb +249 -0
- data/lib/kimurai/base/simple_saver.rb +98 -0
- data/lib/kimurai/base/uniq_checker.rb +22 -0
- data/lib/kimurai/base_helper.rb +22 -0
- data/lib/kimurai/browser_builder.rb +32 -0
- data/lib/kimurai/browser_builder/mechanize_builder.rb +140 -0
- data/lib/kimurai/browser_builder/poltergeist_phantomjs_builder.rb +156 -0
- data/lib/kimurai/browser_builder/selenium_chrome_builder.rb +178 -0
- data/lib/kimurai/browser_builder/selenium_firefox_builder.rb +185 -0
- data/lib/kimurai/capybara_configuration.rb +10 -0
- data/lib/kimurai/capybara_ext/driver/base.rb +62 -0
- data/lib/kimurai/capybara_ext/mechanize/driver.rb +55 -0
- data/lib/kimurai/capybara_ext/poltergeist/driver.rb +13 -0
- data/lib/kimurai/capybara_ext/selenium/driver.rb +24 -0
- data/lib/kimurai/capybara_ext/session.rb +150 -0
- data/lib/kimurai/capybara_ext/session/config.rb +18 -0
- data/lib/kimurai/cli.rb +157 -0
- data/lib/kimurai/cli/ansible_command_builder.rb +71 -0
- data/lib/kimurai/cli/generator.rb +57 -0
- data/lib/kimurai/core_ext/array.rb +14 -0
- data/lib/kimurai/core_ext/numeric.rb +19 -0
- data/lib/kimurai/core_ext/string.rb +7 -0
- data/lib/kimurai/pipeline.rb +25 -0
- data/lib/kimurai/runner.rb +72 -0
- data/lib/kimurai/template/.gitignore +18 -0
- data/lib/kimurai/template/.ruby-version +1 -0
- data/lib/kimurai/template/Gemfile +20 -0
- data/lib/kimurai/template/README.md +3 -0
- data/lib/kimurai/template/config/application.rb +32 -0
- data/lib/kimurai/template/config/automation.yml +13 -0
- data/lib/kimurai/template/config/boot.rb +22 -0
- data/lib/kimurai/template/config/initializers/.keep +0 -0
- data/lib/kimurai/template/config/schedule.rb +57 -0
- data/lib/kimurai/template/db/.keep +0 -0
- data/lib/kimurai/template/helpers/application_helper.rb +3 -0
- data/lib/kimurai/template/lib/.keep +0 -0
- data/lib/kimurai/template/log/.keep +0 -0
- data/lib/kimurai/template/pipelines/saver.rb +11 -0
- data/lib/kimurai/template/pipelines/validator.rb +24 -0
- data/lib/kimurai/template/spiders/application_spider.rb +104 -0
- data/lib/kimurai/template/tmp/.keep +0 -0
- data/lib/kimurai/version.rb +3 -0
- metadata +349 -0
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
3
|
+
class Capybara::Driver::Base
|
4
|
+
attr_accessor :visited
|
5
|
+
attr_writer :requests, :responses
|
6
|
+
|
7
|
+
def requests
|
8
|
+
@requests ||= 0
|
9
|
+
end
|
10
|
+
|
11
|
+
def responses
|
12
|
+
@responses ||= 0
|
13
|
+
end
|
14
|
+
|
15
|
+
def current_memory
|
16
|
+
driver_pid = pid
|
17
|
+
|
18
|
+
all = (get_descendant_processes(driver_pid) << driver_pid).uniq
|
19
|
+
all.map { |pid| get_process_memory(pid) }.sum
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def get_descendant_processes(base)
|
25
|
+
descendants = Hash.new { |ht, k| ht[k] = [k] }
|
26
|
+
Hash[*`ps -eo pid,ppid`.scan(/\d+/).map(&:to_i)].each do |pid, ppid|
|
27
|
+
descendants[ppid] << descendants[pid]
|
28
|
+
end
|
29
|
+
|
30
|
+
descendants[base].flatten - [base]
|
31
|
+
end
|
32
|
+
|
33
|
+
# https://github.com/schneems/get_process_mem
|
34
|
+
# Note: for Linux takes PSS (not RSS) memory (I think PSS better fits in this case)
|
35
|
+
def get_process_memory(pid)
|
36
|
+
case @platform ||= Gem::Platform.local.os
|
37
|
+
when "linux"
|
38
|
+
begin
|
39
|
+
file = Pathname.new "/proc/#{pid}/smaps"
|
40
|
+
return 0 unless file.exist?
|
41
|
+
|
42
|
+
lines = file.each_line.select { |line| line.match(/^Pss/) }
|
43
|
+
return 0 if lines.empty?
|
44
|
+
|
45
|
+
lines.reduce(0) do |sum, line|
|
46
|
+
line.match(/(?<value>(\d*\.{0,1}\d+))\s+(?<unit>\w\w)/) do |m|
|
47
|
+
sum += m[:value].to_i
|
48
|
+
end
|
49
|
+
|
50
|
+
sum
|
51
|
+
end
|
52
|
+
rescue Errno::EACCES
|
53
|
+
0
|
54
|
+
end
|
55
|
+
when "darwin"
|
56
|
+
mem = `ps -o rss= -p #{pid}`.strip
|
57
|
+
mem.empty? ? 0 : mem.to_i
|
58
|
+
else
|
59
|
+
raise "Can't check process memory, wrong type of platform: #{@platform}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'mechanize'
|
2
|
+
require_relative '../driver/base'
|
3
|
+
|
4
|
+
class Capybara::Mechanize::Driver
|
5
|
+
# Extend capybara-mechnize to support Poltergeist-like methods
|
6
|
+
# https://www.rubydoc.info/gems/poltergeist/Capybara/Poltergeist/Driver
|
7
|
+
|
8
|
+
def set_proxy(ip, port, type, user, password)
|
9
|
+
# type is always "http", "socks" is not supported (yet)
|
10
|
+
browser.agent.set_proxy(ip, port, user, password)
|
11
|
+
end
|
12
|
+
|
13
|
+
def headers
|
14
|
+
browser.agent.request_headers
|
15
|
+
end
|
16
|
+
|
17
|
+
def headers=(headers)
|
18
|
+
browser.agent.request_headers = headers
|
19
|
+
end
|
20
|
+
|
21
|
+
def add_header(name, value)
|
22
|
+
browser.agent.request_headers[name] = value
|
23
|
+
end
|
24
|
+
|
25
|
+
def set_cookie(name, value, options = {})
|
26
|
+
options[:name] ||= name
|
27
|
+
options[:value] ||= value
|
28
|
+
|
29
|
+
cookie = Mechanize::Cookie.new(options.merge path: "/")
|
30
|
+
browser.agent.cookie_jar << cookie
|
31
|
+
end
|
32
|
+
|
33
|
+
def clear_cookies
|
34
|
+
browser.agent.cookie_jar.clear!
|
35
|
+
end
|
36
|
+
|
37
|
+
def quit
|
38
|
+
browser.agent.shutdown
|
39
|
+
end
|
40
|
+
|
41
|
+
###
|
42
|
+
|
43
|
+
# Reset parent method `current_memory` for mechanize (we can't measure memory of Mechanize driver)
|
44
|
+
def current_memory
|
45
|
+
nil
|
46
|
+
end
|
47
|
+
|
48
|
+
def pid
|
49
|
+
nil
|
50
|
+
end
|
51
|
+
|
52
|
+
def port
|
53
|
+
nil
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require_relative '../driver/base'
|
2
|
+
|
3
|
+
class Capybara::Selenium::Driver
|
4
|
+
def set_cookie(name, value, options = {})
|
5
|
+
options[:name] ||= name
|
6
|
+
options[:value] ||= value
|
7
|
+
|
8
|
+
browser.manage.add_cookie(options)
|
9
|
+
end
|
10
|
+
|
11
|
+
def clear_cookies
|
12
|
+
browser.manage.delete_all_cookies
|
13
|
+
end
|
14
|
+
|
15
|
+
###
|
16
|
+
|
17
|
+
def pid
|
18
|
+
@pid ||= `lsof -i tcp:#{port} -t`.strip.to_i
|
19
|
+
end
|
20
|
+
|
21
|
+
def port
|
22
|
+
@port ||= browser.send(:bridge).instance_variable_get("@http").instance_variable_get("@server_url").port
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
require 'capybara'
|
2
|
+
require 'nokogiri'
|
3
|
+
require_relative 'session/config'
|
4
|
+
|
5
|
+
module Capybara
|
6
|
+
class Session
|
7
|
+
attr_accessor :spider
|
8
|
+
|
9
|
+
def current_response
|
10
|
+
Nokogiri::HTML(body)
|
11
|
+
end
|
12
|
+
|
13
|
+
alias_method :original_visit, :visit
|
14
|
+
def visit(visit_uri, delay: config.before_request[:delay], skip_request_options: false, max_retries: 3)
|
15
|
+
if spider
|
16
|
+
process_delay(delay) if delay
|
17
|
+
retries, sleep_interval = 0, 0
|
18
|
+
|
19
|
+
begin
|
20
|
+
check_request_options(visit_uri) unless skip_request_options
|
21
|
+
driver.requests += 1 and logger.info "Browser: started get request to: #{visit_uri}"
|
22
|
+
spider.class.update(:visits, :requests) if spider.with_info
|
23
|
+
|
24
|
+
original_visit(visit_uri)
|
25
|
+
rescue *config.retry_request_errors => e
|
26
|
+
logger.error "Browser: request visit error: #{e.inspect}, url: #{visit_uri}"
|
27
|
+
|
28
|
+
if (retries += 1) < max_retries
|
29
|
+
logger.info "Browser: sleep #{(sleep_interval += 15)} seconds and process retry № #{retries} to the url: #{visit_uri}"
|
30
|
+
sleep sleep_interval and retry
|
31
|
+
else
|
32
|
+
logger.error "Browser: all retries (#{retries}) to the url `#{visit_uri}` are gone"
|
33
|
+
raise e
|
34
|
+
end
|
35
|
+
else
|
36
|
+
driver.responses += 1 and logger.info "Browser: finished get request to: #{visit_uri}"
|
37
|
+
spider.class.update(:visits, :responses) if spider.with_info
|
38
|
+
driver.visited = true unless driver.visited
|
39
|
+
ensure
|
40
|
+
if spider.with_info
|
41
|
+
logger.info "Info: visits: requests: #{spider.class.visits[:requests]}, responses: #{spider.class.visits[:responses]}"
|
42
|
+
end
|
43
|
+
|
44
|
+
if memory = driver.current_memory
|
45
|
+
logger.debug "Browser: driver.current_memory: #{memory}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
else
|
49
|
+
original_visit(visit_uri)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def destroy_driver!
|
54
|
+
if @driver
|
55
|
+
@driver.quit
|
56
|
+
@driver = nil
|
57
|
+
logger.info "Browser: driver #{mode} has been destroyed"
|
58
|
+
else
|
59
|
+
logger.warn "Browser: driver #{mode} is not present"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def restart!
|
64
|
+
if mode.match?(/poltergeist/)
|
65
|
+
@driver.browser.restart
|
66
|
+
@driver.requests, @driver.responses = 0, 0
|
67
|
+
else
|
68
|
+
destroy_driver!
|
69
|
+
driver
|
70
|
+
end
|
71
|
+
|
72
|
+
logger.info "Browser: driver has been restarted: name: #{mode}, pid: #{driver.pid}, port: #{driver.port}"
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def process_delay(delay)
|
78
|
+
interval = (delay.class == Range ? rand(delay) : delay)
|
79
|
+
logger.debug "Browser: sleep #{interval.round(2)} #{'second'.pluralize(interval)} before request..."
|
80
|
+
sleep interval
|
81
|
+
end
|
82
|
+
|
83
|
+
def check_request_options(url_to_visit)
|
84
|
+
# restart_if
|
85
|
+
if memory_limit = config.restart_if[:memory_limit]
|
86
|
+
memory = driver.current_memory
|
87
|
+
if memory && memory >= memory_limit
|
88
|
+
logger.warn "Browser: memory_limit #{memory_limit} of driver.current_memory (#{memory}) is exceeded (engine: #{mode})"
|
89
|
+
restart!
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
if requests_limit = config.restart_if[:requests_limit]
|
94
|
+
requests = driver.requests
|
95
|
+
if requests >= requests_limit
|
96
|
+
logger.warn "Browser: requests_limit #{requests_limit} of driver.requests (#{requests}) is exceeded (engine: #{mode})"
|
97
|
+
restart!
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# cookies
|
102
|
+
# (Selenium only) if config.cookies present and browser was just created,
|
103
|
+
# visit url_to_visit first and only then set cookies:
|
104
|
+
if driver.visited.nil? && config.cookies && mode.match?(/selenium/)
|
105
|
+
visit(url_to_visit, skip_request_options: true)
|
106
|
+
config.cookies.each do |cookie|
|
107
|
+
driver.set_cookie(cookie[:name], cookie[:value], cookie)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
if config.before_request[:clear_cookies]
|
112
|
+
driver.clear_cookies
|
113
|
+
logger.debug "Browser: cleared cookies before request"
|
114
|
+
end
|
115
|
+
|
116
|
+
if config.before_request[:clear_and_set_cookies]
|
117
|
+
driver.clear_cookies
|
118
|
+
|
119
|
+
# (Selenium only) if browser is not visited yet any page, visit url_to_visit
|
120
|
+
# first and then set cookies (needs after browser restart):
|
121
|
+
if driver.visited.nil? && mode.match?(/selenium/)
|
122
|
+
visit(url_to_visit, skip_request_options: true)
|
123
|
+
end
|
124
|
+
|
125
|
+
config.cookies.each do |cookie|
|
126
|
+
driver.set_cookie(cookie[:name], cookie[:value], cookie)
|
127
|
+
end
|
128
|
+
|
129
|
+
logger.debug "Browser: cleared and set cookies before request"
|
130
|
+
end
|
131
|
+
|
132
|
+
# user_agent
|
133
|
+
if config.before_request[:change_user_agent]
|
134
|
+
driver.add_header("User-Agent", config.user_agent.call)
|
135
|
+
logger.debug "Browser: changed user_agent before request"
|
136
|
+
end
|
137
|
+
|
138
|
+
# proxy
|
139
|
+
if config.before_request[:change_proxy]
|
140
|
+
proxy_string = config.proxy.call
|
141
|
+
driver.set_proxy(*proxy_string.split(":"))
|
142
|
+
logger.debug "Browser: changed proxy before request"
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def logger
|
147
|
+
spider.logger
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Capybara
|
2
|
+
class SessionConfig
|
3
|
+
attr_accessor :cookies, :proxy, :user_agent
|
4
|
+
attr_writer :retry_request_errors
|
5
|
+
|
6
|
+
def retry_request_errors
|
7
|
+
@retry_request_errors ||= []
|
8
|
+
end
|
9
|
+
|
10
|
+
def restart_if
|
11
|
+
@restart_if ||= {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def before_request
|
15
|
+
@before_request ||= {}
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/kimurai/cli.rb
ADDED
@@ -0,0 +1,157 @@
|
|
1
|
+
require 'thor'
|
2
|
+
|
3
|
+
module Kimurai
|
4
|
+
class CLI < Thor
|
5
|
+
map %w[--version -v] => :__print_version
|
6
|
+
|
7
|
+
desc "generate", "Generator, available types: project, spider, schedule"
|
8
|
+
def generate(generator_type, *args)
|
9
|
+
case generator_type
|
10
|
+
when "project"
|
11
|
+
project_name = args.shift
|
12
|
+
raise "Provide project name to generate a new project" unless project_name.present?
|
13
|
+
Generator.new.generate_project(project_name)
|
14
|
+
when "spider"
|
15
|
+
spider_name = args.shift
|
16
|
+
raise "Provide spider name to generate a spider" unless spider_name.present?
|
17
|
+
Generator.new.generate_spider(spider_name, in_project: inside_project?)
|
18
|
+
when "schedule"
|
19
|
+
Generator.new.generate_schedule
|
20
|
+
else
|
21
|
+
raise "Don't know this generator type: #{generator_type}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
###
|
26
|
+
|
27
|
+
desc "setup", "Setup server"
|
28
|
+
option :port, aliases: :p, type: :string, banner: "Port for ssh connection"
|
29
|
+
option "ask-sudo", type: :boolean, banner: "Provide sudo password for a user to install system-wide packages"
|
30
|
+
option "ask-auth-pass", type: :boolean, banner: "Auth using password"
|
31
|
+
option "ssh-key-path", type: :string, banner: "Auth using ssh key"
|
32
|
+
option :local, type: :boolean, banner: "Run setup on a local machine (Ubuntu only)"
|
33
|
+
def setup(user_host)
|
34
|
+
command = AnsibleCommandBuilder.new(user_host, options, playbook: "setup").get
|
35
|
+
|
36
|
+
pid = spawn *command
|
37
|
+
Process.wait pid
|
38
|
+
end
|
39
|
+
|
40
|
+
desc "deploy", "Deploy project to the server and update cron schedule"
|
41
|
+
option :port, aliases: :p, type: :string, banner: "Port for ssh connection"
|
42
|
+
option "ask-auth-pass", type: :boolean, banner: "Auth using password"
|
43
|
+
option "ssh-key-path", type: :string, banner: "Auth using ssh key"
|
44
|
+
option "repo-url", type: :string, banner: "Repo url"
|
45
|
+
option "repo-key-path", type: :string, banner: "SSH key for a git repo"
|
46
|
+
def deploy(user_host)
|
47
|
+
if !`git status --short`.empty?
|
48
|
+
raise "Deploy: Please commit your changes first"
|
49
|
+
elsif `git remote`.empty?
|
50
|
+
raise "Deploy: Please add remote origin repository to your repo first"
|
51
|
+
elsif !`git rev-list master...origin/master`.empty?
|
52
|
+
raise "Deploy: Please push your commits to the remote origin repo first"
|
53
|
+
end
|
54
|
+
|
55
|
+
repo_url = options["repo-url"] ? options["repo-url"] : `git remote get-url origin`.strip
|
56
|
+
repo_name = repo_url[/\/([^\/]*)\.git/i, 1]
|
57
|
+
|
58
|
+
command = AnsibleCommandBuilder.new(user_host, options, playbook: "deploy",
|
59
|
+
vars: { repo_url: repo_url, repo_name: repo_name, repo_key_path: options["repo-key-path"] }
|
60
|
+
).get
|
61
|
+
|
62
|
+
pid = spawn *command
|
63
|
+
Process.wait pid
|
64
|
+
end
|
65
|
+
|
66
|
+
###
|
67
|
+
|
68
|
+
desc "crawl", "Run a particular spider by it's name"
|
69
|
+
def crawl(spider_name)
|
70
|
+
raise "Can't find Kimurai project" unless inside_project?
|
71
|
+
require './config/boot'
|
72
|
+
|
73
|
+
unless klass = Kimurai.find_by_name(spider_name)
|
74
|
+
raise "Can't find spider with name `#{spider_name}` in the project. " \
|
75
|
+
"To list all available spiders, run: `$ bundle exec kimurai list`"
|
76
|
+
end
|
77
|
+
|
78
|
+
# Set time_zone if exists
|
79
|
+
if time_zone = Kimurai.configuration.time_zone
|
80
|
+
Kimurai.time_zone = time_zone
|
81
|
+
end
|
82
|
+
|
83
|
+
klass.crawl!
|
84
|
+
end
|
85
|
+
|
86
|
+
desc "parse", "Parse url in the particular spider method"
|
87
|
+
option :url, type: :string, required: true, banner: "Url to pass to the method"
|
88
|
+
def parse(spider_name, method_name)
|
89
|
+
raise "Can't find Kimurai project" unless inside_project?
|
90
|
+
require './config/boot'
|
91
|
+
|
92
|
+
unless klass = Kimurai.find_by_name(spider_name)
|
93
|
+
raise "Can't find spider with name `#{spider_name}` in the project. " \
|
94
|
+
"To list all available spiders, run: `$ bundle exec kimurai list`"
|
95
|
+
end
|
96
|
+
|
97
|
+
klass.parse!(method_name, url: options["url"])
|
98
|
+
end
|
99
|
+
|
100
|
+
desc "console", "Start Kimurai console"
|
101
|
+
option :engine, type: :string, banner: "Engine to use"
|
102
|
+
option :url, type: :string, banner: "Url to process"
|
103
|
+
def console(spider_name = nil)
|
104
|
+
require 'pry'
|
105
|
+
require './config/boot' if inside_project?
|
106
|
+
|
107
|
+
if spider_name
|
108
|
+
raise "Can't find Kimurai project" unless inside_project?
|
109
|
+
|
110
|
+
unless klass = Kimurai.find_by_name(spider_name)
|
111
|
+
raise "Can't find spider with name `#{spider_name}` in the project. " \
|
112
|
+
"To list all available spiders, run: `$ bundle exec kimurai list`"
|
113
|
+
end
|
114
|
+
else
|
115
|
+
klass = inside_project? ? ApplicationSpider : ::Kimurai::Base
|
116
|
+
end
|
117
|
+
|
118
|
+
engine = options["engine"]&.delete(":")&.to_sym
|
119
|
+
klass.parse!(:console, engine, url: options["url"])
|
120
|
+
end
|
121
|
+
|
122
|
+
desc "list", "List all available spiders in the current project"
|
123
|
+
def list
|
124
|
+
raise "Can't find Kimurai project" unless inside_project?
|
125
|
+
require './config/boot'
|
126
|
+
|
127
|
+
Kimurai.list.keys.each { |name| puts name }
|
128
|
+
end
|
129
|
+
|
130
|
+
desc "runner", "Run all spiders in the project in queue"
|
131
|
+
option :jobs, aliases: :j, type: :numeric, default: 1, banner: "The number of concurrent jobs"
|
132
|
+
def runner
|
133
|
+
raise "Can't find Kimurai project" unless inside_project?
|
134
|
+
|
135
|
+
jobs = options["jobs"]
|
136
|
+
raise "Jobs count can't be 0" if jobs == 0
|
137
|
+
|
138
|
+
require './config/boot'
|
139
|
+
require 'kimurai/runner'
|
140
|
+
Runner.new(parallel_jobs: jobs).run!
|
141
|
+
end
|
142
|
+
|
143
|
+
desc "--version, -v", "Print the version"
|
144
|
+
def __print_version
|
145
|
+
puts VERSION
|
146
|
+
end
|
147
|
+
|
148
|
+
private
|
149
|
+
|
150
|
+
def inside_project?
|
151
|
+
Dir.exists? "spiders"
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
require_relative 'cli/generator'
|
157
|
+
require_relative 'cli/ansible_command_builder'
|