From 4ba47ac90ad73f6553ae7a599e9f2e79b42f4e45 Mon Sep 17 00:00:00 2001 From: ViViDboarder Date: Thu, 18 Apr 2013 09:09:05 -0700 Subject: [PATCH] Initial commit --- .gitignore | 2 + README.md | 17 ++ Rakefile | 8 + abusetheforce.gemspec | 27 ++++ bin/abusetheforce | 14 ++ bin/atf | 14 ++ lib/abusetheforce.rb | 221 ++++++++++++++++++++++++++ lib/abusetheforce/cli.rb | 291 +++++++++++++++++++++++++++++++++++ lib/abusetheforce/version.rb | 3 + test/test.rb | 11 ++ 10 files changed, 608 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 Rakefile create mode 100644 abusetheforce.gemspec create mode 100755 bin/abusetheforce create mode 100755 bin/atf create mode 100644 lib/abusetheforce.rb create mode 100644 lib/abusetheforce/cli.rb create mode 100644 lib/abusetheforce/version.rb create mode 100644 test/test.rb diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..41b8a58 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +atf.yaml +*.gem diff --git a/README.md b/README.md new file mode 100644 index 0000000..58a6570 --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +AbUse the Force +=============== + +A tool expanding upon [Metaforce](https://github.com/ejholmes/metaforce) for deploying to +multiple orgs as well as simpler setup for use as a pseudo compiler + +Features +======== +* Many + +Why not Metaforce? +================== +* Vim and Sublime plugins (coming soon...) +* Command line configuration management +* Options for deploying or retrieving a sigle file +* Encrypted Passwords coming soon + diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..6ba2d0c --- /dev/null +++ b/Rakefile @@ -0,0 +1,8 @@ +require 'rake/testtask' + +Rake::TestTask.new do |t| + t.libs << 'test' +end + +desc "Run tests" +task :default => :test \ No newline at end of file diff --git a/abusetheforce.gemspec b/abusetheforce.gemspec new file mode 100644 index 0000000..caa41f0 --- /dev/null +++ b/abusetheforce.gemspec @@ -0,0 +1,27 @@ +# -*- encoding: utf-8 -*- +$:.push File.expand_path('../lib', __FILE__) +require 'abusetheforce/version' + +Gem::Specification.new do |s| + s.name = 'abusetheforce' + s.version = AbuseTheForce::VERSION + s.authors = ['Ian'] + s.email = ['ViViDboarder@gmail.com'] + s.homepage = 'https://github.com/ViViDboarder/abusetheforce' + s.summary = %q{A Ruby gem for configuring and interacting with Metaforce} + s.description = %q{A Ruby gem for configuring and interacting with Metaforce} + + #s.rubyforge_project = 'abusetheforce' + + s.files = `git ls-files`.split("\n") + s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") + s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } + s.require_paths = ['lib'] + + s.add_dependency 'thor', '~> 0.16.0' + s.add_dependency 'listen', '~> 0.6.0' + s.add_dependency 'metaforce', '~> 1.0.7' + s.add_dependency 'highline' + + s.add_development_dependency 'rake' +end diff --git a/bin/abusetheforce b/bin/abusetheforce new file mode 100755 index 0000000..28ded4a --- /dev/null +++ b/bin/abusetheforce @@ -0,0 +1,14 @@ +#!/usr/bin/env ruby +require 'rubygems' +#require 'bundler/setup' +require 'abusetheforce' + +begin + require 'abusetheforce/cli' + AbuseTheForce::AtfCLI.start +rescue Interrupt => e + puts "\nQuitting..." + exit 1 +rescue SystemExit => e + exit e.status +end diff --git a/bin/atf b/bin/atf new file mode 100755 index 0000000..28ded4a --- /dev/null +++ b/bin/atf @@ -0,0 +1,14 @@ +#!/usr/bin/env ruby +require 'rubygems' +#require 'bundler/setup' +require 'abusetheforce' + +begin + require 'abusetheforce/cli' + AbuseTheForce::AtfCLI.start +rescue Interrupt => e + puts "\nQuitting..." + exit 1 +rescue SystemExit => e + exit e.status +end diff --git a/lib/abusetheforce.rb b/lib/abusetheforce.rb new file mode 100644 index 0000000..d51385e --- /dev/null +++ b/lib/abusetheforce.rb @@ -0,0 +1,221 @@ +require "metaforce" +require "base64" + +module AbuseTheForce + attr_accessor :client + + # Write error to screen + def self.pute(s, fatal=false) + puts "Error: #{s}" + + # If fatal error, exit + if fatal + exit 1 + end + end + + # Write warning to screen + def self.putw(s) + puts "Warning: #{s}" + end + + # builds the client instance of Metaforce + def self.build_client + target = Atf_Config.active_target + + @client = Metaforce.new :username => target.username, + :password => target.get_password, + :security_token => target.security_token + + Metaforce.configuration.host = target.host + end + + # Fetches a single file from the server + def self.retrieve_file(metadata_type, full_name) + + if @client == nil + build_client + end + + manifest = Metaforce::Manifest.new(metadata_type => [full_name]) + + @client.retrieve_unpackaged(manifest). + extract_to(Atf_Config.src). + on_complete { |job| puts "Finished retrieve #{job.id}!" }. + on_error { |job| puts "Something bad happened!" }. + perform + end + + # Fetches a whole project from the server + def self.retrieve_project() + + if @client == nil + build_client + end + + if File.file?(Atf_Config.src + '/package.xml') + @client.retrieve_unpackaged(File.expand_path(Atf_Config.src + '/package.xml')). + extract_to(Atf_Config.src). + on_complete { |job| puts "Finished retrieve #{job.id}!" }. + on_error { |job| puts "Something bad happened!" }. + perform + else + puts "#{Atf_Config.src}: Not a valid project path" + end + end + + def self.deploy_project(dpath=Atf_Config.src) + + if @client == nil + build_client + end + + if File.file?(dpath + '/package.xml') + @client.deploy(File.expand_path(dpath)). + on_complete { |job| puts "Finished deploy #{job.id}!" }. + on_error { |job| puts "Something bad happened!" }. + perform + else + puts "#{dpath}: Not a valid project path" + end + + end + + class Atf_Config + class << self + attr_accessor :targets, :active_target, :src + SETTINGS_FILE="./atf.yaml" + + # Loads configurations from yaml + def load() + + if File.file?(SETTINGS_FILE) == false + puts "No settings file found, creating one now" + # Settings file doesn't exist + # Create it + @targets = {} + @active_target = nil + @src = './src' + + dump_settings + else + settings = YAML.load_file(SETTINGS_FILE) + @targets = settings[:targets] + @src = settings[:src] + end + + # Set the default target + @targets.values.each do |target| + # Check if this one is active + if target.active == true + # Set it if there is no default target set yet + if @active_target == nil + @active_target = target + else + puts "Two active targets set. Using #{@active_target.print}" + end + end + end + end + + # write settings to a yaml file + def dump_settings + File.open(SETTINGS_FILE, 'w') do |out| + YAML.dump( { :targets => @targets, :src => @src }, out) + end + end + + # Adds a new target to the config + def add_target(target) + + # If there are no targets yet, use this one as the default + if @active_target == nil #@targets.empty? + target.active = true + @active_target = target + end + + # Push the new target + @targets[target.name] = target + + #write out the config + dump_settings + + end + + # Selects one target as the active target for deployment + def set_active_target(name) + + # Empty out current active target + @active_target = nil + + # Go through each pair + @targets.each_pair do |target_name, target| + # If you find a matching item + if name == target_name + # Check if there is already a default + if @active_target == nil + # Set active + target.active = true + # Make it default + @active_target = target + else + # Error since there are two defaults + AbuseTheForce.putw "Two defaults set. Using #{@active_target.print}" + end + else # name != name + # make not active + target.active = false + end + end + + if @active_target != nil + # Save to yaml + dump_settings + else + AbuseTheForce.pute "Target with alias #{name} was not found." + end + end + + # Sets project path from default ./src + def set_project_path(ppath) + if File.file?(ppath + '/package.xml') + @src = ppath + dump_settings + else + pute("No package.xml found in #{ppath}", true) + end + end + + + end + end + + # Class for holding a target + class Atf_Target + attr_accessor :name, :username, :password, :security_token, :host, :active + + def initialize(name, username, password, security_token, host="login.salesforce.com") + @name = name + @username = username + set_password(password) + @security_token = security_token + @host = host + @active = false; + end + + # TODO: Provide 2 way encryption with a lock to decode passwords + def set_password(password) + @password = Base64.encode64(password) + end + + def get_password() + return Base64.decode64(@password) + end + + def print + puts "#{@name}\t#{@username}\t#{@host}\t#{(@active && 'Active') || ''}" + end + end + +end + diff --git a/lib/abusetheforce/cli.rb b/lib/abusetheforce/cli.rb new file mode 100644 index 0000000..15c0569 --- /dev/null +++ b/lib/abusetheforce/cli.rb @@ -0,0 +1,291 @@ +require 'thor' +require 'highline/import' + +module AbuseTheForce + + # MODULE METHODS + + # Toggle to a new target temporarily + def self.temp_switch_target(name=nil) + # If a name was provided, switch to that + if name != nil + # Store the original target's name + @last_target = Atf_Config.active_target.name + # Switch to the new target + Atf_Config.set_active_target(name) + else + # Switch back to the old target + Atf_Config.set_active_target(@last_target) + end + end + + # Safe prompt for password + def self.get_password(prompt="Enter Password: ") + ask(prompt) {|q| q.echo = false} + end + + class TargetCLI < Thor + + desc "add [--sandbox]", "Adds a new remote target" + long_desc <<-LONG_DESC + Adds a new target org with the alias to your atf.yaml file + with the provided , and prompted password. + + With -s or --sandbox option, sets host to "test.salesforce.com" + + To perform any actions you must have a valid target added + LONG_DESC + option :sandbox, :type => :boolean, :aliases => :s, :default => false + def add(name, username, security_token) + puts "Add! and prompt pass" + password = AbuseTheForce.get_password() + host = (options[:sandbox] ? "test.salesforce.com" : "login.salesforce.com") + + # Add the target to the config + Atf_Config.add_target(Atf_Target.new(name, username, password, security_token, host)) + end + + desc "update [--password | --sandbox= | --securitytoken=]", "Updates a remote target" + long_desc <<-LONG_DESC + Updates a part of target with + + -p or --password option, prompts you for an updated password + + -s or --sandbox option, switches the host to sandbox or production + + --token= option, sets the security token to the value provided + + The changes will then be written to the atf.yaml file + LONG_DESC + option :password, :type => :boolean, :aliases => :p, :default => false, :desc => "Prompt for password" + option :token, :banner => "" + option :sandbox, :type => :boolean, :aliases => :s, :desc => "Add this if deploying to a sandbox" + def update(name) + + target = Atf_Config.targets[name] + + if target != nil + if options[:password] + target.set_password(AbuseTheForce.get_password) + end + if options[:token] != nil + target.security_token = options[:token] + end + if options[:sandbox] != nil + target.host = (options[:sandbox] ? "test.salesforce.com" : "login.salesforce.com") + end + else + AbuseTheForce.pute("Target not found", true) + end + + # Save to yaml + Atf_Config.dump_settings + end + + desc "remove ", "Removes a remote target" + def remove(name) + Atf_Config.targets.delete(name) + # Save to yaml + Atf_Config.dump_settings + end + + desc "activate ", "Activates specified target" + long_desc <<-LONG_DESC + Activates the target specified by . + + You must have an active target to perform any actions + LONG_DESC + def activate(name) + # Activate the target + Atf_Config.set_active_target(name) + end + + desc "current", "Shows currently active target" + def current() + + if Atf_Config.active_target != nil + Atf_Config.active_target.print + else + AbuseTheForce.putw "No active target set" + end + end + + desc "list", "Lists all targets" + def list() + puts "Name\tUsername\tHost" + + Atf_Config.targets.values.each do |target| + target.print + end + + end + + # By default, display the current target + default_task :current + end + + class DeployCLI < Thor + class_option :target, :banner => "", :aliases => :t + #class_option :delete, :aliases => :d + + TEMP_DIR="./.atf_tmp" + + desc "file ", "Deploy a single file" + long_desc <<-LONG_DESC + Deploys file at path to the active target. + LONG_DESC + def file(fpath) + + # If a new target was provided, switch to it + if options[:target] != nil + AbuseTheForce.temp_switch_target(options[:target]) + end + + # Clear temp dir + if Dir.exists?('./.atf_tmp') + FileUtils.rm_r('./.atf_tmp') + end + # Make it again + Dir.mkdir('./.atf_tmp') + + mdir = File.dirname(fpath).split('/').last + + FileUtils.copy(Atf_Config.src + '/package.xml', TEMP_DIR + '/package.xml') + + FileUtils.mkdir_p('./.atf_tmp/' + mdir) + basename = File.basename(fpath) + FileUtils.cp(Atf_Config.src + '/' + mdir + '/' + basename, TEMP_DIR + '/' + mdir + '/') + FileUtils.cp(Atf_Config.src + '/' + mdir + '/' + basename + '-meta.xml', TEMP_DIR + '/' + mdir + '/') + + AbuseTheForce.deploy_project('./.atf_tmp') + + # if using a temp target, switch back + if options[:target] != nil + AbuseTheForce.temp_switch_target + end + end + + desc "project", "Deploy a whole project" + def project() + + # If a new target was provided, switch to it + if options[:target] != nil + AbuseTheForce.temp_switch_target(options[:target]) + end + # Deploy the project + AbuseTheForce.deploy_project() + + # if using a temp target, switch back + if options[:target] != nil + AbuseTheForce.temp_switch_target + end + end + end + + class RetrieveCLI < Thor + class_option :target, :banner => "", :aliases => :t + #class_option :delete, :aliases => :d + + desc "file [metadata type]", "Retrieve a single file" + long_desc <<-LONG_DESC + Retrieves one file from the active target + + This has two uses: + + To retrieve a file not on the local machine, provide the name of the + file and the metadata type. + + Example: $atf retrieve file MyClassName ApexClass + + To retrieve a new version of a file already local to you, you just + provide the path to the file. + + Example: $atf retrieve file ./src/classes/MyClass + + NOTE: Must be called from the root project directory + LONG_DESC + def file(full_name, metadata_type=nil) + # TODO: Work backwards if called not in child of project directory + + # If a new target was provided, switch to it + if options[:target] != nil + AbuseTheForce.temp_switch_target(options[:target]) + end + + # No metadata passed in, this should be a file path + if metadata_type == nil + if File.file?(full_name) + # Get the file extension + extname = File.extname(full_name) + # Get the base name of the metadata + full_name = File.basename(full_name, extname) + + puts full_name + puts extname + # Detect metadata type by file extension + case extname + when '.cls' + metadata_type = 'ApexClass' + when '.trigger' + metadata_type = 'ApexTrigger' + when '.object' + metadata_type = 'CustomObject' + when '.page' + metadata_type = 'ApexPage' + when '.component' + metadata_type = 'ApexComponent' + else + AbuseTheForce.pute('Unrecognized file type', true) + end + end + end + + # retrieve the file using metaforce + AbuseTheForce.retrieve_file(metadata_type, full_name) + + # if using a temp target, switch back + if options[:target] != nil + AbuseTheForce.temp_switch_target + end + end + + desc "project", "Retrieve a whole project" + def project() + + # If a new target was provided, switch to it + if options[:target] != nil + AbuseTheForce.temp_switch_target(options[:target]) + end + + # Retrieve the project + AbuseTheForce.retrieve_project + + # if using a temp target, switch back + if options[:target] != nil + AbuseTheForce.temp_switch_target + end + end + end + + # AbUse The Force + class AtfCLI < Thor + + # Load the abuse-the-force config + Atf_Config.load + + desc "target SUBCOMMAND ...ARGS", "Manage deploy targets" + subcommand "target", TargetCLI + + desc "deploy SUBCOMMAND ...ARGS", "Deploy code to Salesforce.com" + subcommand "deploy", DeployCLI + + desc "retrieve SUBCOMMAND ...ARGS", "Retrieve code from Salesforce.com" + subcommand "retrieve", RetrieveCLI + end + + # Start the command line + #AtfCLI.start(ARGV) + +end # end module AbuseTheForce + + diff --git a/lib/abusetheforce/version.rb b/lib/abusetheforce/version.rb new file mode 100644 index 0000000..ea842d1 --- /dev/null +++ b/lib/abusetheforce/version.rb @@ -0,0 +1,3 @@ +module AbuseTheForce + VERSION = '0.0.0' +end diff --git a/test/test.rb b/test/test.rb new file mode 100644 index 0000000..b9ebded --- /dev/null +++ b/test/test.rb @@ -0,0 +1,11 @@ +require 'test/unit' +require 'abusetheforce' + +class AbuseTheForceTest < Test::Unit::TestCase + + def test_first_test + # TODO: Unstub this + assert_equal "a", "a" + end + +end