claide-plugins 0.9.0

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,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