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