bootic_cli 0.1.9 → 0.2.0.pre1

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