claide-plugins 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,34 @@
1
+ require 'claide/command/plugins_helper'
2
+ require 'claide/command/gem_helper'
3
+
4
+ module CLAide
5
+ class Command
6
+ class Plugins
7
+ # The list subcommand. Used to list all known plugins
8
+ #
9
+ class List < Plugins
10
+ self.summary = 'List all known plugins'
11
+ self.description = <<-DESC
12
+ List all known plugins (according to the list
13
+ hosted on github.com/CocoaPods/cocoapods-plugins)
14
+ DESC
15
+
16
+ def self.options
17
+ super.reject { |option, _| option == '--silent' }
18
+ end
19
+
20
+ def run
21
+ plugins = PluginsHelper.known_plugins
22
+ GemHelper.download_and_cache_specs if self.verbose?
23
+
24
+ name = CLAide::Plugins.config.name
25
+ UI.title "Available #{name} Plugins:" do
26
+ plugins.each do |plugin|
27
+ PluginsHelper.print_plugin plugin, self.verbose?
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,60 @@
1
+ require 'claide/command/plugins_helper'
2
+ require 'claide/command/gem_helper'
3
+ require 'claide/command'
4
+
5
+ module CLAide
6
+ class Command
7
+ class Plugins
8
+ # The search subcommand.
9
+ # Used to search a plugin in the list of known plugins,
10
+ # searching into the name, author description fields
11
+ #
12
+ class Search < Plugins
13
+ self.summary = 'Search for known plugins'
14
+ self.description = <<-DESC
15
+ Searches plugins whose 'name' contains the given `QUERY`.
16
+ `QUERY` is a regular expression, ignoring case.
17
+
18
+ With `--full`, it also searches by 'author' and 'description'.
19
+ DESC
20
+
21
+ self.arguments = [
22
+ CLAide::Argument.new('QUERY', true),
23
+ ]
24
+
25
+ def self.options
26
+ [
27
+ ['--full', 'Search by name, author, and description'],
28
+ ].concat(super.reject { |option, _| option == '--silent' })
29
+ end
30
+
31
+ def initialize(argv)
32
+ @full_text_search = argv.flag?('full')
33
+ @query = argv.shift_argument unless argv.arguments.empty?
34
+ super
35
+ end
36
+
37
+ def validate!
38
+ super
39
+ help! 'A search query is required.' if @query.nil? || @query.empty?
40
+ begin
41
+ /#{@query}/
42
+ rescue RegexpError
43
+ help! 'A valid regular expression is required.'
44
+ end
45
+ end
46
+
47
+ def run
48
+ plugins = PluginsHelper.matching_plugins(@query, @full_text_search)
49
+ GemHelper.download_and_cache_specs if self.verbose?
50
+
51
+ name = CLAide::Plugins.config.name
52
+ UI.title "Available #{name} Plugins matching '#{@query}':"
53
+ plugins.each do |plugin|
54
+ PluginsHelper.print_plugin plugin, self.verbose?
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,35 @@
1
+ require 'claide'
2
+
3
+ module CLAide
4
+ module Plugins
5
+ class Configuration
6
+ # name of the plugin
7
+ attr_accessor :name
8
+
9
+ # prefix to use when searching for gems to load at runtime
10
+ attr_accessor :plugin_prefix
11
+
12
+ # url for JSON file that holds list of plugins to show when searching
13
+ attr_accessor :plugin_list_url
14
+
15
+ # url for repo that holds template to use when creating a new plugin
16
+ attr_accessor :plugin_template_url
17
+
18
+ def initialize(name = 'default name',
19
+ plugin_prefix = 'claide',
20
+ plugin_list_url = 'https://github.com/cocoapods/claide-plugins/something.json',
21
+ plugin_template_url = 'https://github.com/cocoapods/claide-plugins-template')
22
+ @name = name
23
+ @plugin_prefix = plugin_prefix
24
+ @plugin_list_url = plugin_list_url
25
+ @plugin_template_url = plugin_template_url
26
+ end
27
+ end
28
+
29
+ class << self
30
+ attr_accessor :config
31
+ end
32
+ # set a default configuration that will work with claide-plugins
33
+ self.config = Configuration.new
34
+ end
35
+ end
@@ -0,0 +1,134 @@
1
+ require 'claide/command/gem_helper'
2
+
3
+ module CLAide
4
+ class Command
5
+ # This module is used by Command::Plugins::List
6
+ # and Command::Plugins::Search to download and parse
7
+ # the JSON describing the plugins list and manipulate it
8
+ #
9
+ module PluginsHelper
10
+ def self.plugins_raw_url
11
+ CLAide::Plugins.config.plugin_list_url
12
+ end
13
+
14
+ # Force-download the JSON
15
+ #
16
+ # @return [Hash] The hash representing the JSON with all known plugins
17
+ #
18
+ def self.download_json
19
+ UI.puts 'Downloading Plugins list...'
20
+ response = REST.get(plugins_raw_url)
21
+ if response.ok?
22
+ parse_json(response.body)
23
+ else
24
+ raise Informative, 'Could not download plugins list ' \
25
+ "from cocoapods-plugins: #{response.inspect}"
26
+ end
27
+ end
28
+
29
+ # The list of all known plugins, according to
30
+ # the JSON hosted on github's cocoapods-plugins
31
+ #
32
+ # @return [Array] all known plugins, as listed in the downloaded JSON
33
+ #
34
+ def self.known_plugins
35
+ json = download_json
36
+ json['plugins']
37
+ end
38
+
39
+ # Filter plugins to return only matching ones
40
+ #
41
+ # @param [String] query
42
+ # A query string that corresponds to a valid RegExp pattern.
43
+ #
44
+ # @param [Bool] full_text_search
45
+ # false only searches in the plugin's name.
46
+ # true searches in the plugin's name, author and description.
47
+ #
48
+ # @return [Array] all plugins matching the query
49
+ #
50
+ def self.matching_plugins(query, full_text_search)
51
+ query_regexp = /#{query}/i
52
+ known_plugins.reject do |plugin|
53
+ texts = [plugin['name']]
54
+ if full_text_search
55
+ texts << plugin['author'] if plugin['author']
56
+ texts << plugin['description'] if plugin['description']
57
+ end
58
+ texts.grep(query_regexp).empty?
59
+ end
60
+ end
61
+
62
+ # Display information about a plugin
63
+ #
64
+ # @param [Hash] plugin
65
+ # The hash describing the plugin
66
+ #
67
+ # @param [Bool] verbose
68
+ # If true, will also print the author of the plugins.
69
+ # Defaults to false.
70
+ #
71
+ def self.print_plugin(plugin, verbose = false)
72
+ plugin_colored_name = plugin_title(plugin)
73
+
74
+ UI.title(plugin_colored_name, '', 1) do
75
+ UI.puts_indented plugin['description']
76
+ ljust = verbose ? 16 : 11
77
+ UI.labeled('Gem', plugin['gem'], ljust)
78
+ UI.labeled('URL', plugin['url'], ljust)
79
+ print_verbose_plugin(plugin, ljust) if verbose
80
+ end
81
+ end
82
+
83
+ #----------------#
84
+
85
+ private
86
+
87
+ # Smaller helper to print out the verbose details
88
+ # for a plugin.
89
+ #
90
+ # @param [Hash] plugin
91
+ # The hash describing the plugin
92
+ #
93
+ # @param [Integer] ljust
94
+ # The left justification that is passed into UI.labeled
95
+ #
96
+ def self.print_verbose_plugin(plugin, ljust)
97
+ UI.labeled('Author', plugin['author'], ljust)
98
+ unless GemHelper.cache.specs.empty?
99
+ versions = GemHelper.versions_string(plugin['gem'])
100
+ UI.labeled('Versions', versions, ljust)
101
+ end
102
+ end
103
+
104
+ # Parse the given JSON data, handling parsing errors if any
105
+ #
106
+ # @param [String] json_str
107
+ # The string representation of the JSON to parse
108
+ #
109
+ def self.parse_json(json_str)
110
+ JSON.parse(json_str)
111
+ rescue JSON::ParserError => e
112
+ raise Informative, "Invalid plugins list from cocoapods-plugins: #{e}"
113
+ end
114
+
115
+ # Format the title line to print the plugin info with print_plugin
116
+ # coloring it according to whether the plugin is installed or not
117
+ #
118
+ # @param [Hash] plugin
119
+ # The hash describing the plugin
120
+ #
121
+ # @return [String] The formatted and colored title
122
+ #
123
+ def self.plugin_title(plugin)
124
+ plugin_name = "-> #{plugin['name']}"
125
+ if GemHelper.gem_installed?(plugin['gem'])
126
+ plugin_name += " (#{GemHelper.installed_version(plugin['gem'])})"
127
+ plugin_name.green
128
+ else
129
+ plugin_name.yellow
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,116 @@
1
+ module CLAide
2
+ # Module which provides support for running executables.
3
+ #
4
+ # In a class it can be used as:
5
+ #
6
+ # extend Executable
7
+ # executable :git
8
+ #
9
+ # This will create two methods `git` and `git!` both accept a command but
10
+ # the latter will raise on non successful executions. The methods return the
11
+ # output of the command.
12
+ #
13
+ module Executable
14
+ # Creates the methods for the executable with the given name.
15
+ #
16
+ # @param [Symbol] name
17
+ # the name of the executable.
18
+ #
19
+ # @return [void]
20
+ #
21
+ def executable(name)
22
+ define_method(name) do |*command|
23
+ Executable.execute_command(name, Array(command).flatten, false)
24
+ end
25
+
26
+ define_method(name.to_s + '!') do |*command|
27
+ Executable.execute_command(name, Array(command).flatten, true)
28
+ end
29
+ end
30
+
31
+ # Executes the given command. Displays output if in verbose mode.
32
+ #
33
+ # @param [String] bin
34
+ # The binary to use.
35
+ #
36
+ # @param [Array<#to_s>] command
37
+ # The command to send to the binary.
38
+ #
39
+ # @param [Bool] raise_on_failure
40
+ # Whether it should raise if the command fails.
41
+ #
42
+ # @raise If the executable could not be located.
43
+ #
44
+ # @raise If the command fails and the `raise_on_failure` is set to true.
45
+ #
46
+ # @return [String] the output of the command (STDOUT and STDERR).
47
+ #
48
+ # @todo Find a way to display the live output of the commands.
49
+ #
50
+ def self.execute_command(exe, command, raise_on_failure)
51
+ bin = `which #{exe}`.strip
52
+ raise Informative, "Unable to locate `#{exe}`" if bin.empty?
53
+
54
+ require 'open4'
55
+ require 'shellwords'
56
+
57
+ command = command.map(&:to_s)
58
+ full_command = \
59
+ "#{bin.shellescape} #{command.map(&:shellescape).join(' ')}"
60
+
61
+ # if Config.instance.verbose?
62
+ # UI.message("$ #{full_command}")
63
+ # stdout, stderr = Indenter.new(STDOUT), Indenter.new(STDERR)
64
+ # else
65
+ stdout, stderr = Indenter.new, Indenter.new
66
+ # end
67
+
68
+ options = { :stdout => stdout, :stderr => stderr, :status => true }
69
+ status = Open4.spawn(bin, command, options)
70
+ output = stdout.join("\n") + stderr.join("\n")
71
+ unless status.success?
72
+ if raise_on_failure
73
+ raise Informative, "#{full_command}\n\n#{output}"
74
+ else
75
+ UI.message("[!] Failed: #{full_command}".red)
76
+ end
77
+ end
78
+ output
79
+ end
80
+
81
+ #-------------------------------------------------------------------------#
82
+
83
+ # Helper class that allows to write to an {IO} instance taking into account
84
+ # the UI indentation level.
85
+ #
86
+ class Indenter < ::Array
87
+ # @return [Fixnum] The indentation level of the UI.
88
+ #
89
+ attr_accessor :indent
90
+
91
+ # @return [IO] the {IO} to which the output should be printed.
92
+ #
93
+ attr_accessor :io
94
+
95
+ # @param [IO] io @see io
96
+ #
97
+ def initialize(io = nil)
98
+ @io = io
99
+ @indent = ' ' * UI.indentation_level
100
+ end
101
+
102
+ # Stores a portion of the output and prints it to the {IO} instance.
103
+ #
104
+ # @param [String] value
105
+ # the output to print.
106
+ #
107
+ # @return [void]
108
+ #
109
+ def <<(value)
110
+ super
111
+ ensure
112
+ @io << "#{ indent }#{ value }" if @io
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1 @@
1
+ require 'claide/command/plugins'
@@ -0,0 +1,3 @@
1
+ module CLAidePlugins
2
+ VERSION = '0.9.0'.freeze
3
+ end
@@ -0,0 +1,41 @@
1
+ require File.expand_path('../spec_helper', File.dirname(__FILE__))
2
+ require 'claide/command/gem_helper'
3
+
4
+ # The CLAide namespace
5
+ #
6
+ module CLAide
7
+ describe Command::GemHelper do
8
+ before do
9
+ UI_OUT.reopen
10
+ end
11
+
12
+ after do
13
+ mocha_teardown
14
+ end
15
+
16
+ it 'detects if a gem is installed' do
17
+ Command::GemHelper.gem_installed?('bacon').should.be.true
18
+ Command::GemHelper.gem_installed?('fake-fake-fake-gem').should.be.false
19
+ end
20
+
21
+ it 'detects if a specific version of a gem is installed' do
22
+ Command::GemHelper.gem_installed?('bacon', Bacon::VERSION).should.be.true
23
+ impossibacon = Gem::Version.new(Bacon::VERSION).bump
24
+ Command::GemHelper.gem_installed?('bacon', impossibacon).should.be.false
25
+ end
26
+
27
+ it 'creates a version list that includes all versions of a single gem' do
28
+ spec2 = Gem::NameTuple.new('cocoapods-plugins', Gem::Version.new('0.2.0'))
29
+ spec1 = Gem::NameTuple.new('cocoapods-plugins', Gem::Version.new('0.1.0'))
30
+ response = [{ 1 => [spec2, spec1] }, []]
31
+ Gem::SpecFetcher.any_instance.stubs(:available_specs).returns(response)
32
+
33
+ @cache = Command::GemIndexCache.new
34
+ @cache.download_and_cache_specs
35
+ versions_string =
36
+ Command::GemHelper.versions_string('cocoapods-plugins', @cache)
37
+ versions_string.should.include('0.2.0')
38
+ versions_string.should.include('0.1.0')
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,38 @@
1
+ require File.expand_path('../spec_helper', File.dirname(__FILE__))
2
+
3
+ # The CocoaPods namespace
4
+ #
5
+ module CLAide
6
+ describe Command::GemIndexCache do
7
+ before do
8
+ @cache = Command::GemIndexCache.new
9
+ UI_OUT.reopen
10
+ end
11
+
12
+ after do
13
+ mocha_teardown
14
+ end
15
+
16
+ it 'notifies the user that it is downloading the spec index' do
17
+ response = [{}, []]
18
+ Gem::SpecFetcher.any_instance.stubs(:available_specs).returns(response)
19
+
20
+ @cache.download_and_cache_specs
21
+ out = UI_OUT.string
22
+ out.should.include('Downloading Rubygem specification index...')
23
+ out.should.not.include('Error downloading Rubygem specification')
24
+ end
25
+
26
+ it 'notifies the user when getting the spec index fails' do
27
+ error = Gem::RemoteFetcher::FetchError.new('no host', 'bad url')
28
+ wrapper_error = stub(:error => error)
29
+ response = [[], [wrapper_error]]
30
+ Gem::SpecFetcher.any_instance.stubs(:available_specs).returns(response)
31
+
32
+ @cache.download_and_cache_specs
33
+ @cache.specs.should.be.empty?
34
+ UI_OUT.string.should.include('Downloading Rubygem specification index...')
35
+ UI_OUT.string.should.include('Error downloading Rubygem specification')
36
+ end
37
+ end
38
+ end