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 +4 -4
- data/bootic_cli.gemspec +3 -0
- data/lib/bootic_cli/cli.rb +1 -1
- data/lib/bootic_cli/{cli → commands}/orders.rb +0 -0
- data/lib/bootic_cli/commands/themes.rb +118 -0
- data/lib/bootic_cli/themes/api_theme.rb +96 -0
- data/lib/bootic_cli/themes/fs_theme.rb +107 -0
- data/lib/bootic_cli/themes/mem_theme.rb +50 -0
- data/lib/bootic_cli/themes/missing_items_theme.rb +27 -0
- data/lib/bootic_cli/themes/theme_diff.rb +31 -0
- data/lib/bootic_cli/themes/theme_selector.rb +84 -0
- data/lib/bootic_cli/themes/updated_theme.rb +62 -0
- data/lib/bootic_cli/themes/workflows.rb +294 -0
- data/lib/bootic_cli/version.rb +1 -1
- metadata +57 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 912137c9b982caccf6e5c4fbe6a0cf7aff82cf5e
|
4
|
+
data.tar.gz: 0a33582b05a274372b79c60b65b32d37e34097dd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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"
|
data/lib/bootic_cli/cli.rb
CHANGED
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
|
+
|
data/lib/bootic_cli/version.rb
CHANGED
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.
|
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-
|
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:
|
198
|
+
version: 1.3.1
|
148
199
|
requirements: []
|
149
200
|
rubyforge_project:
|
150
|
-
rubygems_version: 2.
|
201
|
+
rubygems_version: 2.6.13
|
151
202
|
signing_key:
|
152
203
|
specification_version: 4
|
153
204
|
summary: Bootic command line.
|