bootic_cli 0.1.9 → 0.2.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 688263fa607dd97eb2335475ce3d9240906004ba
4
- data.tar.gz: 7394dd12856ee4890df1de84d38e0afeab0069cb
3
+ metadata.gz: 912137c9b982caccf6e5c4fbe6a0cf7aff82cf5e
4
+ data.tar.gz: 0a33582b05a274372b79c60b65b32d37e34097dd
5
5
  SHA512:
6
- metadata.gz: ba9be7df26eeba72b2838e53428c2bd639ac0ffae48f79b909d59bd31f42b73091f9cf99ffb505952ab55baca1c2e73bdfc0333b50af0c2fe864c5ee3b316cf9
7
- data.tar.gz: 7b6214feac9fcde7b16fa2a74c04b20af28e877d09274aca885a05a50861e4a72cd1224b92aa8df9092781082804c0da18a5e619bb20be6b0bdafde8f77c6896
6
+ metadata.gz: eb4aafc997f9bda07cf351ca67fccdc89de4e241e80d079111e4585406be075ef1af4979d1d636b4f121389b1215416d91a511a55e56fadf1f2d0a595aaee91a
7
+ data.tar.gz: 9725a2def7ec18acad6e18abf2a35fffc9615acfca0ac72aa5042c5b959989beea68e471c320112755fca38d815231bfb818fd976f6ded4744e350490e4f2a5d
data/bootic_cli.gemspec CHANGED
@@ -21,6 +21,9 @@ Gem::Specification.new do |spec|
21
21
 
22
22
  spec.add_dependency 'thor'
23
23
  spec.add_dependency 'bootic_client', "~> 0.0.19"
24
+ spec.add_dependency 'diffy', "~> 3.2"
25
+ spec.add_dependency 'listen', "~> 3.1"
26
+ spec.add_dependency 'launchy'
24
27
 
25
28
  spec.add_development_dependency "bundler", "~> 1.9"
26
29
  spec.add_development_dependency "rake", "~> 10.0"
@@ -137,7 +137,7 @@ module BooticCli
137
137
 
138
138
  require "bootic_cli/command"
139
139
 
140
- Dir[File.join(File.dirname(__FILE__), 'cli', '*.rb')].each do |f|
140
+ Dir[File.join(File.dirname(__FILE__), 'commands', '*.rb')].each do |f|
141
141
  load_file f
142
142
  end
143
143
 
File without changes
@@ -0,0 +1,118 @@
1
+ require 'launchy'
2
+ require 'bootic_cli/themes/workflows'
3
+ require 'bootic_cli/themes/theme_selector'
4
+
5
+ module BooticCli
6
+ module Commands
7
+ class Themes < BooticCli::Command
8
+ desc 'pull [shop] [dir]', 'Pull latest theme changes in [shop] into directory [dir] (current by default)'
9
+ option :destroy, banner: '<true|false>', type: :boolean, default: true
10
+ option :public, banner: '<true|false>', type: :boolean, default: false, aliases: '-p'
11
+ def pull(subdomain = nil, dir = '.')
12
+ logged_in_action do
13
+ local_theme, remote_theme = theme_selector.select_theme_pair(subdomain, dir, options['public'])
14
+ workflows.pull(local_theme, remote_theme, destroy: options['destroy'])
15
+ end
16
+ end
17
+
18
+ desc 'push [shop] [dir]', 'Push all local theme files in [dir] to remote shop [shop]'
19
+ option :public, banner: '<true|false>', type: :boolean, default: false, aliases: '-p'
20
+ option :destroy, banner: '<true|false>', type: :boolean, default: true
21
+ def push(subdomain = nil, dir = '.')
22
+ logged_in_action do
23
+ local_theme, remote_theme = theme_selector.select_theme_pair(subdomain, dir, options['public'])
24
+ workflows.push(local_theme, remote_theme, destroy: options['destroy'])
25
+ end
26
+ end
27
+
28
+ desc 'sync [shop] [dir]', 'Sync local theme copy in [dir] with remote [shop]'
29
+ option :public, banner: '<true|false>', type: :boolean, default: false, aliases: '-p'
30
+ def sync(subdomain = nil, dir = '.')
31
+ logged_in_action do
32
+ local_theme, remote_theme = theme_selector.select_theme_pair(subdomain, dir, options['public'])
33
+ workflows.sync(local_theme, remote_theme)
34
+ end
35
+ end
36
+
37
+ desc 'compare [shop] [dir]', 'Show differences between local and remote copies'
38
+ option :public, banner: '<true|false>', type: :boolean, default: false, aliases: '-p'
39
+ def compare(subdomain = nil, dir = '.')
40
+ logged_in_action do
41
+ local_theme, remote_theme = theme_selector.select_theme_pair(subdomain, dir, options['public'])
42
+ workflows.compare(local_theme, remote_theme)
43
+ end
44
+ end
45
+
46
+ desc 'watch [shop] [dir]', 'Watch theme directory at [dir] and create/update/delete the one in [shop] when changed'
47
+ option :public, banner: '<true|false>', type: :boolean, default: false, aliases: '-p'
48
+ def watch(subdomain = nil, dir = '.')
49
+ logged_in_action do
50
+ _, remote_theme = theme_selector.select_theme_pair(subdomain, dir, options['public'])
51
+ workflows.watch(dir, remote_theme)
52
+ end
53
+ end
54
+
55
+ desc 'publish [shop] [dir]', 'Publish dev files to public theme'
56
+ def publish(subdomain = nil, dir = '.')
57
+ logged_in_action do
58
+ local_theme, remote_theme = theme_selector.select_theme_pair(subdomain, dir, false)
59
+ workflows.publish(local_theme, remote_theme)
60
+ end
61
+ end
62
+
63
+ desc 'open [shop] [dir]', 'Open theme in a browser'
64
+ option :public, banner: '<true|false>', type: :boolean, default: false, aliases: '-p'
65
+ def open(subdomain = nil, dir = '.')
66
+ logged_in_action do
67
+ _, remote_theme = theme_selector.select_theme_pair(subdomain, dir, options['public'])
68
+ Launchy.open remote_theme.href
69
+ end
70
+ end
71
+
72
+ private
73
+
74
+ def prompt
75
+ @prompt ||= Prompt.new
76
+ end
77
+
78
+ def workflows
79
+ BooticCli::Themes::Workflows.new(prompt: prompt)
80
+ end
81
+
82
+ def theme_selector
83
+ @theme_selector ||= BooticCli::Themes::ThemeSelector.new(root, prompt: prompt)
84
+ end
85
+
86
+ class Prompt
87
+ def initialize
88
+ @shell = Thor::Shell::Color.new
89
+ end
90
+
91
+ def yes_or_no?(question, default_answer)
92
+ default_char = default_answer ? 'y' : 'n'
93
+ input = shell.ask("\n#{question} [#{default_char}]").strip
94
+ return default_answer if input == '' || input.downcase == default_char
95
+ !default_answer
96
+ end
97
+
98
+ def notice(str)
99
+ parts = [" --->", str]
100
+ highlight parts.join(' ')
101
+ end
102
+
103
+ def highlight(str, color = :bold)
104
+ say str, color
105
+ end
106
+
107
+ def say(str, color = nil)
108
+ shell.say str, color
109
+ end
110
+
111
+ private
112
+ attr_reader :shell
113
+ end
114
+
115
+ declare self, 'manage shop themes'
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,96 @@
1
+ require 'time'
2
+ require 'open-uri'
3
+
4
+ module BooticCli
5
+ module Themes
6
+ class ItemWithTime < SimpleDelegator
7
+ def updated_on
8
+ Time.parse(super)
9
+ end
10
+ end
11
+
12
+ class APIAsset < ItemWithTime
13
+ def file
14
+ @file ||= open(rels[:file].href)
15
+ end
16
+ end
17
+
18
+ class APITheme
19
+ def initialize(theme)
20
+ @theme = theme
21
+ end
22
+
23
+ # this is unique to API themes
24
+ def publish(clone = false)
25
+ if theme.can?(:publish_theme)
26
+ @theme = theme.publish_theme
27
+ @theme.create_dev_theme if clone
28
+ reload!(false)
29
+ end
30
+ end
31
+
32
+ def href
33
+ theme.rels[:theme_preview].href
34
+ end
35
+
36
+ # Implement generic Theme interface
37
+ def reload!(refetch = true)
38
+ @templates = nil
39
+ @assets = nil
40
+ @theme = theme.self if refetch
41
+ self
42
+ end
43
+
44
+ def templates
45
+ @templates ||= theme.templates.map{|t| ItemWithTime.new(t) }
46
+ end
47
+
48
+ def assets
49
+ @assets ||= theme.assets.map{|t| APIAsset.new(t) }
50
+ end
51
+
52
+ def add_template(file_name, body)
53
+ check_errors! theme.create_template(
54
+ file_name: file_name,
55
+ body: body
56
+ )
57
+ end
58
+
59
+ def remove_template(file_name)
60
+ tpl = theme.templates.find { |t| t.file_name == file_name }
61
+ check_errors!(tpl.delete_template) if tpl && tpl.can?(:delete_template)
62
+ end
63
+
64
+ def add_asset(file_name, file)
65
+ check_errors! theme.create_theme_asset(
66
+ file_name: file_name,
67
+ data: file
68
+ )
69
+ end
70
+
71
+ def remove_asset(file_name)
72
+ asset = theme.assets.find{|t| t.file_name == file_name }
73
+ check_errors!(asset.delete_theme_asset) if asset
74
+ end
75
+
76
+ private
77
+ attr_reader :theme
78
+
79
+ class EntityErrors < StandardError
80
+ attr_reader :errors
81
+ def initialize(errors)
82
+ @errors = errors
83
+ super "Entity has errors: #{errors.map(&:field)}"
84
+ end
85
+ end
86
+
87
+ def check_errors!(entity)
88
+ if entity.has?(:errors)
89
+ raise EntityErrors.new(entity.errors)
90
+ end
91
+
92
+ entity
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,107 @@
1
+ require 'fileutils'
2
+
3
+ module BooticCli
4
+ module Themes
5
+ class FSTheme
6
+ Template = Struct.new(:file_name, :body, :updated_on)
7
+ ThemeAsset = Struct.new(:file_name, :file, :updated_on)
8
+
9
+ ASSETS_DIR = 'assets'.freeze
10
+ TEMPLATE_PATTERNS = ['*.liquid', '*.html', '*.css', '*.js', 'theme.yml'].freeze
11
+ ASSET_PATTERNS = [File.join(ASSETS_DIR, '*')].freeze
12
+
13
+ # helper to resolve the right type (Template or Asset) from a local path
14
+ # this is not part of the generic Theme interface
15
+ def self.resolve_type(path)
16
+ path =~ /assets\// ? :asset : :template
17
+ end
18
+
19
+ def self.resolve_file(path)
20
+ file = File.new(path)
21
+ file_name = File.basename(path)
22
+ type = resolve_type(path)
23
+
24
+ item = if path =~ /assets\//
25
+ ThemeAsset.new(file_name, file.read, file.mtime.utc)
26
+ else
27
+ Template.new(file_name, file.read, file.mtime.utc)
28
+ end
29
+
30
+ [item, type]
31
+ end
32
+
33
+ def initialize(dir)
34
+ FileUtils.mkdir_p dir
35
+ FileUtils.mkdir_p File.join(dir, ASSETS_DIR)
36
+ @dir = dir
37
+ end
38
+
39
+ # Implement generic Theme interface
40
+ def reload!
41
+ @templates = nil
42
+ @assets = nil
43
+ end
44
+
45
+ def templates
46
+ @templates ||= (
47
+ paths_for(TEMPLATE_PATTERNS).sort.map do |path|
48
+ name = File.basename(path)
49
+ file = File.new(path)
50
+ Template.new(name, file.read, file.mtime.utc)
51
+ end
52
+ )
53
+ end
54
+
55
+ def assets
56
+ @assets ||= (
57
+ paths_for(ASSET_PATTERNS).sort.map do |path|
58
+ fname = File.basename(path)
59
+ file = File.new(path)
60
+ ThemeAsset.new(fname, file, file.mtime.utc)
61
+ end
62
+ )
63
+ end
64
+
65
+ def add_template(file_name, body)
66
+ path = File.join(dir, file_name)
67
+
68
+ File.open(path, 'w') do |io|
69
+ io.write body
70
+ end
71
+ @templates = nil
72
+ end
73
+
74
+ def remove_template(file_name)
75
+ path = File.join(dir, file_name)
76
+ return false unless File.exists?(path)
77
+ File.unlink path
78
+ @templates = nil
79
+ end
80
+
81
+ def add_asset(file_name, file)
82
+ path = File.join(dir, ASSETS_DIR, file_name)
83
+ File.open(path, 'wb') do |io|
84
+ io.write file.read
85
+ end
86
+
87
+ @assets = nil
88
+ end
89
+
90
+ def remove_asset(file_name)
91
+ path = File.join(dir, ASSETS_DIR, file_name)
92
+ return false unless File.exists?(path)
93
+
94
+ File.unlink path
95
+ @assets = nil
96
+ end
97
+
98
+ private
99
+
100
+ attr_reader :dir
101
+
102
+ def paths_for(patterns)
103
+ patterns.reduce([]) {|m, pattern| m + Dir[File.join(dir, pattern)]}
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,50 @@
1
+ module BooticCli
2
+ module Themes
3
+ class MemTheme
4
+ Template = Struct.new(:file_name, :body, :updated_on)
5
+ ThemeAsset = Struct.new(:file_name, :file, :updated_on)
6
+
7
+ def initialize
8
+ reload!
9
+ end
10
+
11
+ # Implement generic Theme interface
12
+ attr_reader :templates, :assets
13
+
14
+ def reload!
15
+ @templates = []
16
+ @assets = []
17
+ end
18
+
19
+ def add_template(file_name, body, mtime: Time.now)
20
+ tpl = Template.new(file_name, body, mtime)
21
+ if idx = templates.index{|t| t.file_name == file_name }
22
+ templates[idx] = tpl
23
+ else
24
+ templates << tpl
25
+ end
26
+ end
27
+
28
+ def remove_template(file_name)
29
+ if idx = templates.index{|t| t.file_name == file_name }
30
+ templates.delete_at idx
31
+ end
32
+ end
33
+
34
+ def add_asset(file_name, file, mtime: Time.now)
35
+ asset = ThemeAsset.new(file_name, file, mtime)
36
+ if idx = assets.index{|t| t.file_name == file_name }
37
+ assets[idx] = asset
38
+ else
39
+ assets << asset
40
+ end
41
+ end
42
+
43
+ def remove_asset(file_name)
44
+ if idx = assets.index{|t| t.file_name == file_name }
45
+ assets.delete_at idx
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,27 @@
1
+ module BooticCli
2
+ module Themes
3
+ class MissingItemsTheme
4
+ def initialize(source:, target:)
5
+ @source, @target = source, target
6
+ end
7
+
8
+ def templates
9
+ @templates ||= find_missing_files(source.templates, target.templates)
10
+ end
11
+
12
+ def assets
13
+ @assets ||= find_missing_files(source.assets, target.assets)
14
+ end
15
+
16
+ private
17
+ attr_reader :source, :target
18
+
19
+ def find_missing_files(set1, set2)
20
+ file_names = set2.map(&:file_name)
21
+ set1.select do |f|
22
+ !file_names.include?(f.file_name)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,31 @@
1
+ require 'bootic_cli/themes/updated_theme'
2
+ require 'bootic_cli/themes/missing_items_theme'
3
+
4
+ module BooticCli
5
+ module Themes
6
+ class ThemeDiff
7
+ def initialize(source:, target:)
8
+ @source, @target = source, target
9
+ end
10
+
11
+ def updated_in_source
12
+ @updated_in_source ||= UpdatedTheme.new(source: source, target: target)
13
+ end
14
+
15
+ def updated_in_target
16
+ @updated_in_target ||= UpdatedTheme.new(source: target, target: source)
17
+ end
18
+
19
+ def missing_in_target
20
+ @missing_in_target ||= MissingItemsTheme.new(source: source, target: target)
21
+ end
22
+
23
+ def missing_in_source
24
+ @missing_in_source ||= MissingItemsTheme.new(source: target, target: source)
25
+ end
26
+
27
+ private
28
+ attr_reader :source, :target
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,84 @@
1
+ require 'yaml/store'
2
+ require 'bootic_cli/themes/api_theme'
3
+ require 'bootic_cli/themes/fs_theme'
4
+
5
+ module BooticCli
6
+ module Themes
7
+ class ThemeSelector
8
+ def initialize(root, prompt:)
9
+ @root = root
10
+ @prompt = prompt
11
+ end
12
+
13
+ def select_theme_pair(subdomain, dir, production = false)
14
+ local_theme = select_local_theme(dir)
15
+ st = YAML::Store.new(File.join(File.expand_path(dir), '.state'))
16
+ shop = resolve_and_store_shop(subdomain, dir)
17
+ remote_theme = resolve_remote_theme(shop, production)
18
+ prompt.say "Preview remote theme at #{remote_theme.rels[:theme_preview].href}", :yellow
19
+ [local_theme, APITheme.new(remote_theme)]
20
+ end
21
+
22
+ def select_local_theme(dir)
23
+ FSTheme.new(File.expand_path(dir))
24
+ end
25
+
26
+ def resolve_and_store_shop(subdomain, dir)
27
+ st = YAML::Store.new(File.join(File.expand_path(dir), '.state'))
28
+ st.transaction do
29
+ sub = st['subdomain']
30
+ if sub
31
+ shop = find_remote_shop(sub)
32
+ raise "No shop could be resolved with subdomain: #{subdomain} and dir: #{dir}" unless shop
33
+ shop
34
+ else # no subdomain stored yet. Resolve and store.
35
+ shop = resolve_shop_from_subdomain_or_dir(subdomain, dir)
36
+ st['subdomain'] = shop.subdomain
37
+ shop
38
+ end
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def find_remote_shop(subdomain)
45
+ if root.has?(:all_shops)
46
+ root.all_shops(subdomains: subdomain).first
47
+ else
48
+ root.shops.find { |s| s.subdomain == subdomain }
49
+ end
50
+ end
51
+
52
+ def resolve_shop_from_subdomain_or_dir(subdomain, dir)
53
+ shop = if subdomain
54
+ find_remote_shop(subdomain)
55
+ elsif dir
56
+ subdomain = File.basename(dir)
57
+ find_remote_shop(subdomain)
58
+ end
59
+ shop || root.shops.first
60
+ end
61
+
62
+ def resolve_remote_theme(shop, production = false)
63
+ if production
64
+ prompt.say "Working on production theme", :red
65
+ return shop.theme
66
+ end
67
+
68
+ prompt.say "Working on development theme", :green
69
+ themes = shop.themes
70
+ if themes.has?(:dev_theme)
71
+ themes.dev_theme
72
+ elsif themes.can?(:create_dev_theme)
73
+ prompt.say "Creating development theme...", :green
74
+ themes.create_dev_theme
75
+ else
76
+ raise "No dev theme available"
77
+ end
78
+ end
79
+
80
+ private
81
+ attr_reader :root, :prompt
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,62 @@
1
+ require 'diffy'
2
+
3
+ module BooticCli
4
+ module Themes
5
+ # given :source and :target themes,
6
+ # UpdatedTheme computes assets and templates
7
+ # with more recent versions in :source
8
+ class UpdatedTheme
9
+ TemplateWithDiff = Struct.new('TemplateWithDiff', :file_name, :body, :updated_on, :diff)
10
+
11
+ def initialize(source:, target:)
12
+ @source, @target = source, target
13
+ end
14
+
15
+ def templates
16
+ @templates ||= map_pair(source.templates, target.templates) do |a, b|
17
+ diff = Diffy::Diff.new(normalize_endings(b.body), normalize_endings(a.body), context: 1)
18
+ if more_recent?(a, b) && !diff.to_s.empty?
19
+ c = TemplateWithDiff.new(a.file_name, a.body, a.updated_on, diff)
20
+ [true, c]
21
+ else
22
+ [false, nil]
23
+ end
24
+ end
25
+ end
26
+
27
+ def assets
28
+ @assets ||= map_pair(source.assets, target.assets) do |a, b|
29
+ [more_recent?(a, b), a]
30
+ end
31
+ end
32
+
33
+ private
34
+ attr_reader :source, :target
35
+
36
+ def more_recent?(a, b)
37
+ a.updated_on > b.updated_on
38
+ end
39
+
40
+ def build_lookup(list)
41
+ list.each_with_object({}) do |item, lookup|
42
+ lookup[item.file_name] = item
43
+ end
44
+ end
45
+
46
+ def map_pair(list1, list2, &block)
47
+ lookup = build_lookup(list2)
48
+ list1.each_with_object([]) do |item, arr|
49
+ match = lookup[item.file_name]
50
+ if match
51
+ valid, item = yield(item, match)
52
+ arr << item if valid
53
+ end
54
+ end
55
+ end
56
+
57
+ def normalize_endings(str)
58
+ str.to_s.gsub(/\r\n?/, "\n")
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,294 @@
1
+ require 'listen'
2
+ require 'thread'
3
+ require 'bootic_cli/themes/theme_diff'
4
+ require 'bootic_cli/themes/fs_theme'
5
+
6
+ module BooticCli
7
+ module Themes
8
+ class NullPrompt
9
+ def self.yes_or_no?(str, default)
10
+ true
11
+ end
12
+
13
+ def self.notice(str)
14
+
15
+ end
16
+
17
+ def self.highlight(str)
18
+
19
+ end
20
+
21
+ def self.say(*_)
22
+
23
+ end
24
+ end
25
+
26
+ class Workflows
27
+ def initialize(prompt: NullPrompt)
28
+ @prompt = prompt
29
+ end
30
+
31
+ def pull(local_theme, remote_theme, destroy: true)
32
+ diff = ThemeDiff.new(source: local_theme, target: remote_theme)
33
+ check_dupes!(local_theme.assets)
34
+
35
+ download_opts = {
36
+ overwrite: false,
37
+ interactive: true
38
+ }
39
+
40
+ notice 'Updating local templates...'
41
+ maybe_update(diff.updated_in_target.templates, 'remote', 'local') do |t|
42
+ local_theme.add_template t.file_name, t.body
43
+ end
44
+
45
+ if destroy
46
+ notice 'Removing local files that were removed on remote...'
47
+ remove_all(diff.missing_in_target, local_theme)
48
+ else
49
+ notice 'Not removing local files that were removed on remote.'
50
+ end
51
+
52
+ notice 'Pulling missing files from remote...'
53
+ copy_templates(diff.missing_in_source, local_theme, download_opts)
54
+ # lets copy all of them and let user decide to overwrite existing
55
+ copy_assets(remote_theme, local_theme, download_opts)
56
+ end
57
+
58
+ def push(local_theme, remote_theme, destroy: true)
59
+ diff = ThemeDiff.new(source: local_theme, target: remote_theme)
60
+ check_dupes!(local_theme.assets)
61
+
62
+ notice 'Pushing local changes to remote...'
63
+
64
+ # update existing templates
65
+ notice 'Updating remote templates...'
66
+ maybe_update(diff.updated_in_source.templates, 'local', 'remote') do |t|
67
+ remote_theme.add_template t.file_name, t.body
68
+ end
69
+
70
+ notice 'Pushing files that are missing in remote...'
71
+ copy_assets(diff.missing_in_target, remote_theme, overwrite: true)
72
+ copy_templates(diff.missing_in_target, remote_theme)
73
+
74
+ if destroy
75
+ notice 'Removing remote files that were removed locally...'
76
+ remove_all(diff.missing_in_source, remote_theme)
77
+ else
78
+ notice 'Not removing remote files that were removed locally.'
79
+ end
80
+ end
81
+
82
+ def sync(local_theme, remote_theme)
83
+ diff = ThemeDiff.new(source: local_theme, target: remote_theme)
84
+ check_dupes!(local_theme.assets)
85
+ notice 'Syncing local copy with remote...'
86
+
87
+ download_opts = {
88
+ overwrite: false,
89
+ interactive: false
90
+ }
91
+
92
+ # first, update existing templates in each side
93
+ notice 'Updating local templates...'
94
+ maybe_update(diff.updated_in_target.templates, 'remote', 'local') do |t|
95
+ local_theme.add_template t.file_name, t.body
96
+ end
97
+
98
+ notice 'Updating remote templates...'
99
+ maybe_update(diff.updated_in_source.templates, 'local', 'remote') do |t|
100
+ remote_theme.add_template t.file_name, t.body
101
+ end
102
+
103
+ # now, download missing files on local end
104
+ notice 'Downloading missing local templates & assets...'
105
+ copy_templates(diff.missing_in_source, local_theme, download_opts)
106
+ copy_assets(diff.missing_in_source, local_theme, overwrite: true)
107
+
108
+ # now, upload missing files on remote
109
+ notice 'Uploading missing remote templates & assets...'
110
+ copy_templates(diff.missing_in_target, remote_theme, download_opts)
111
+ copy_assets(diff.missing_in_target, remote_theme, overwrite: true)
112
+ end
113
+
114
+ def compare(local_theme, remote_theme)
115
+ diff = ThemeDiff.new(source: local_theme, target: remote_theme)
116
+ notice 'Comparing local and remote copies of theme...'
117
+
118
+ notice "Local <--- Remote"
119
+
120
+ diff.updated_in_target.templates.each do |t|
121
+ puts "Updated in remote: #{t.file_name}"
122
+ end
123
+
124
+ diff.missing_in_source.templates.each do |t|
125
+ puts "Remote template not in local dir: #{t.file_name}"
126
+ end
127
+
128
+ diff.missing_in_source.assets.each do |t|
129
+ puts "Remote asset not in local dir: #{t.file_name}"
130
+ end
131
+
132
+ notice "Local ---> Remote"
133
+
134
+ diff.updated_in_source.templates.each do |t|
135
+ puts "Updated locally: #{t.file_name}"
136
+ end
137
+
138
+ diff.missing_in_target.templates.each do |f|
139
+ puts "Local template not in remote: #{f.file_name}"
140
+ end
141
+
142
+ diff.missing_in_target.assets.each do |f|
143
+ puts "Local asset not in remote: #{f.file_name}"
144
+ end
145
+ end
146
+
147
+ def watch(dir, remote_theme, watcher: Listen)
148
+ listener = watcher.to(dir) do |modified, added, removed|
149
+ if modified.any?
150
+ modified.each do |path|
151
+ upsert_file remote_theme, path
152
+ end
153
+ end
154
+
155
+ if added.any?
156
+ added.each do |path|
157
+ upsert_file remote_theme, path
158
+ end
159
+ end
160
+
161
+ if removed.any?
162
+ removed.each do |path|
163
+ delete_file remote_theme, path
164
+ end
165
+ end
166
+
167
+ # update local cache
168
+ remote_theme.reload!
169
+ end
170
+
171
+ notice "Watching #{File.expand_path(dir)} for changes..."
172
+ listener.start
173
+
174
+ # ctrl-c
175
+ Signal.trap('INT') {
176
+ listener.stop
177
+ puts 'See you in another lifetime, brotha.'
178
+ exit
179
+ }
180
+
181
+ Kernel.sleep
182
+ end
183
+
184
+ def publish(local_theme, remote_theme)
185
+ keep_old_theme = !prompt.yes_or_no?("Do you want to keep the dev theme as the old public one?", false)
186
+ # first push local files to dev theme
187
+ prompt.say "pushing local changes to development theme"
188
+ push local_theme, remote_theme, destroy: true
189
+ # now publish remote dev theme
190
+ # let it fail if remote_theme doesn't respond to #publish
191
+ prompt.notice "publishing development theme"
192
+ remote_theme.publish(!keep_old_theme) # swap remote themes
193
+ prompt.highlight "published to #{remote_theme.href}", :yellow
194
+ end
195
+
196
+ private
197
+ attr_reader :prompt
198
+
199
+ def check_dupes!(list)
200
+ names = list.map { |f| f.file_name.downcase }
201
+ dupes = names.group_by { |e| e }.select { |k, v| v.size > 1 }.map(&:first)
202
+
203
+ dupes.each do |downcased|
204
+ arr = list.select { |f| f.file_name.downcase == downcased }
205
+ highlight(" --> Name clash between files: " + arr.map(&:file_name).join(', '), :red)
206
+ end
207
+
208
+ if dupes.any?
209
+ highlight("Please ensure there are no name clashes before continuing. Thanks!")
210
+ abort
211
+ end
212
+ end
213
+
214
+ def maybe_update(modified_templates, source_name, target_name, &block)
215
+ modified_templates.each do |t|
216
+ puts "---------"
217
+ puts "#{source_name} #{t.file_name} was modified at #{t.updated_on} (more recent than #{target_name}):"
218
+ puts "---------"
219
+ puts t.diff.to_s(:color)
220
+
221
+ yield(t) if prompt.yes_or_no?("Update #{target_name} #{t.file_name}?", true)
222
+ end
223
+ end
224
+
225
+ def notice(str)
226
+ prompt.notice str
227
+ end
228
+
229
+ def puts(str)
230
+ prompt.say str
231
+ end
232
+
233
+ def highlight(str)
234
+ prompt.highlight str
235
+ end
236
+
237
+ def remove_all(from, to)
238
+ from.templates.each { |f| to.remove_template(f.file_name) }
239
+ from.assets.each { |f| to.remove_asset(f.file_name) }
240
+ end
241
+
242
+ def copy_templates(from, to, opts = {})
243
+ from.templates.each do |t|
244
+ to.add_template t.file_name, t.body
245
+ puts "Copied #{t.file_name}"
246
+ end
247
+ end
248
+
249
+ def copy_assets(from, to, opts = {})
250
+ files = from.assets.find_all do |a|
251
+ if opts[:overwrite]
252
+ true
253
+ else
254
+ target_asset = to.assets.find{ |t| t.file_name == a.file_name }
255
+ if target_asset
256
+ opts[:interactive] && prompt.yes_or_no?("Asset exists: #{a.file_name}. Overwrite?", false)
257
+ else
258
+ true
259
+ end
260
+ end
261
+ end
262
+
263
+ files.each do |a|
264
+ to.add_asset a.file_name, a.file
265
+ puts "Copied asset #{a.file_name}"
266
+ end
267
+ end
268
+
269
+ def upsert_file(theme, path)
270
+ item, type = FSTheme.resolve_file(path)
271
+ case type
272
+ when :template
273
+ theme.add_template item.file_name, item.body
274
+ when :asset
275
+ theme.add_asset item.file_name, item.file
276
+ end
277
+ puts "Uploaded #{type}: #{item.file_name}"
278
+ end
279
+
280
+ def delete_file(theme, path)
281
+ type = FSTheme.resolve_type(path)
282
+ file_name = File.basename(path)
283
+ case type
284
+ when :template
285
+ theme.remove_template file_name
286
+ when :asset
287
+ theme.remove_asset file_name
288
+ end
289
+ puts "Deleted remote #{type}: #{file_name}"
290
+ end
291
+ end
292
+ end
293
+ end
294
+
@@ -1,3 +1,3 @@
1
1
  module BooticCli
2
- VERSION = "0.1.9"
2
+ VERSION = "0.2.0.pre1"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bootic_cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.9
4
+ version: 0.2.0.pre1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ismael Celis
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-08-02 00:00:00.000000000 Z
11
+ date: 2017-12-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -38,6 +38,48 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: 0.0.19
41
+ - !ruby/object:Gem::Dependency
42
+ name: diffy
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.2'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.2'
55
+ - !ruby/object:Gem::Dependency
56
+ name: listen
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.1'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.1'
69
+ - !ruby/object:Gem::Dependency
70
+ name: launchy
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
41
83
  - !ruby/object:Gem::Dependency
42
84
  name: bundler
43
85
  requirement: !ruby/object:Gem::Requirement
@@ -115,14 +157,23 @@ files:
115
157
  - exe/btc
116
158
  - lib/bootic_cli.rb
117
159
  - lib/bootic_cli/cli.rb
118
- - lib/bootic_cli/cli/orders.rb
119
160
  - lib/bootic_cli/command.rb
161
+ - lib/bootic_cli/commands/orders.rb
162
+ - lib/bootic_cli/commands/themes.rb
120
163
  - lib/bootic_cli/connectivity.rb
121
164
  - lib/bootic_cli/console.rb
122
165
  - lib/bootic_cli/file_runner.rb
123
166
  - lib/bootic_cli/formatters.rb
124
167
  - lib/bootic_cli/session.rb
125
168
  - lib/bootic_cli/store.rb
169
+ - lib/bootic_cli/themes/api_theme.rb
170
+ - lib/bootic_cli/themes/fs_theme.rb
171
+ - lib/bootic_cli/themes/mem_theme.rb
172
+ - lib/bootic_cli/themes/missing_items_theme.rb
173
+ - lib/bootic_cli/themes/theme_diff.rb
174
+ - lib/bootic_cli/themes/theme_selector.rb
175
+ - lib/bootic_cli/themes/updated_theme.rb
176
+ - lib/bootic_cli/themes/workflows.rb
126
177
  - lib/bootic_cli/version.rb
127
178
  homepage: http://www.bootic.net
128
179
  licenses:
@@ -142,12 +193,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
142
193
  version: '0'
143
194
  required_rubygems_version: !ruby/object:Gem::Requirement
144
195
  requirements:
145
- - - ">="
196
+ - - ">"
146
197
  - !ruby/object:Gem::Version
147
- version: '0'
198
+ version: 1.3.1
148
199
  requirements: []
149
200
  rubyforge_project:
150
- rubygems_version: 2.5.1
201
+ rubygems_version: 2.6.13
151
202
  signing_key:
152
203
  specification_version: 4
153
204
  summary: Bootic command line.