aem_lookout 0.0.1
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.
- data/Gemfile +3 -0
- data/Gemfile.lock +31 -0
- data/LICENSE.txt +22 -0
- data/README.md +33 -0
- data/Rakefile +1 -0
- data/aem_lookout.gemspec +26 -0
- data/bin/aem_lookout +2 -0
- data/bin/lookout +38 -0
- data/example_config.json +21 -0
- data/lib/aem_lookout/lookout_error.rb +3 -0
- data/lib/aem_lookout/sling_initial_content_converter.rb +98 -0
- data/lib/aem_lookout/sync.rb +186 -0
- data/lib/aem_lookout/terminal.rb +41 -0
- data/lib/aem_lookout/version.rb +3 -0
- data/lib/aem_lookout/watcher.rb +223 -0
- data/lib/aem_lookout.rb +17 -0
- data/spec/lib/aem_lookout/sling_initial_content_converter_spec.rb +91 -0
- data/spec/lib/aem_lookout/sync_spec.rb +4 -0
- data/spec/lib/aem_lookout_spec.rb +9 -0
- data/spec/spec_helper.rb +10 -0
- data/sync.sh +9 -0
- metadata +154 -0
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
aem_lookout (0.0.1)
|
5
|
+
builder (~> 3.2.2)
|
6
|
+
rb-fsevent (~> 0.9.4)
|
7
|
+
vlt_wrapper (~> 2.4.18)
|
8
|
+
|
9
|
+
GEM
|
10
|
+
remote: https://rubygems.org/
|
11
|
+
specs:
|
12
|
+
builder (3.2.2)
|
13
|
+
diff-lcs (1.2.5)
|
14
|
+
rb-fsevent (0.9.4)
|
15
|
+
rspec (2.14.1)
|
16
|
+
rspec-core (~> 2.14.0)
|
17
|
+
rspec-expectations (~> 2.14.0)
|
18
|
+
rspec-mocks (~> 2.14.0)
|
19
|
+
rspec-core (2.14.7)
|
20
|
+
rspec-expectations (2.14.5)
|
21
|
+
diff-lcs (>= 1.1.3, < 2.0)
|
22
|
+
rspec-mocks (2.14.5)
|
23
|
+
vlt_wrapper (2.4.18)
|
24
|
+
|
25
|
+
PLATFORMS
|
26
|
+
ruby
|
27
|
+
|
28
|
+
DEPENDENCIES
|
29
|
+
bundler (~> 1.5)
|
30
|
+
aem_lookout!
|
31
|
+
rspec (~> 2.14.1)
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Jordan Raine
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# AemLookout
|
2
|
+
|
3
|
+
Automatically deploys code changes while developing.
|
4
|
+
|
5
|
+
A short loop between when you make changes and when you see those changes is an important step to productivity!
|
6
|
+
|
7
|
+
## Install
|
8
|
+
|
9
|
+
```
|
10
|
+
gem install aem_lookout
|
11
|
+
```
|
12
|
+
|
13
|
+
or add the following to your Gemfile:
|
14
|
+
|
15
|
+
```
|
16
|
+
gem 'aem_lookout'
|
17
|
+
```
|
18
|
+
|
19
|
+
and run `bundle install` from your shell.
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
1. Create a lookout.json file for your code base, similar to the (example config file)[https://github.com/jnraine/aem_lookout/blob/master/example_config.json].
|
24
|
+
2. Run `lookout` from the command line. May need to run `bundle exec lookout` if the executable is not found.
|
25
|
+
3. Edit files — .java, .css, .content.xml, whatever — and watch them deploy to your local instance.
|
26
|
+
|
27
|
+
## Contributing
|
28
|
+
|
29
|
+
1. Fork it ( http://github.com/<my-github-username>/aem_lookout/fork )
|
30
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
31
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
32
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
33
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/aem_lookout.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'aem_lookout/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "aem_lookout"
|
8
|
+
spec.version = AemLookout::VERSION
|
9
|
+
spec.authors = ["Jordan Raine"]
|
10
|
+
spec.email = ["jnraine@gmail.com"]
|
11
|
+
spec.summary = %q{Speeds up iteration loop while developing for AEM/CQ.}
|
12
|
+
spec.homepage = ""
|
13
|
+
spec.license = "MIT"
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0")
|
16
|
+
spec.executables = ["aem_lookout", "lookout"]
|
17
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_runtime_dependency "vlt_wrapper", "~> 2.4.18"
|
21
|
+
spec.add_runtime_dependency "builder", "~> 3.2.2"
|
22
|
+
spec.add_runtime_dependency "rb-fsevent", "~> 0.9.4"
|
23
|
+
|
24
|
+
spec.add_development_dependency "bundler", "~> 1.5"
|
25
|
+
spec.add_development_dependency "rspec", "~> 2.14.1"
|
26
|
+
end
|
data/bin/aem_lookout
ADDED
data/bin/lookout
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$: << File.dirname(__FILE__) + "/lib"
|
4
|
+
require 'aem_lookout'
|
5
|
+
require 'rb-fsevent'
|
6
|
+
require 'optparse'
|
7
|
+
require 'json'
|
8
|
+
|
9
|
+
options = {}
|
10
|
+
option_parser = OptionParser.new do |opts|
|
11
|
+
opts.banner = "Usage: #{File.basename(__FILE__)} [-p CQ_REPO_PATH]"
|
12
|
+
|
13
|
+
opts.on("-p", "--path CQ_REPO_PATH", "Path to source code repo") do |cq_repo_path|
|
14
|
+
options[:cq_repo_path] = cq_repo_path
|
15
|
+
end
|
16
|
+
|
17
|
+
opts.on_tail("-h", "--help", "Show this message") do
|
18
|
+
puts opts
|
19
|
+
exit
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
option_parser.parse!
|
24
|
+
|
25
|
+
begin
|
26
|
+
@cq_repo_path = options.fetch(:cq_repo_path, File.expand_path("."))
|
27
|
+
rescue KeyError
|
28
|
+
puts option_parser
|
29
|
+
exit 1
|
30
|
+
end
|
31
|
+
|
32
|
+
def config
|
33
|
+
template = File.read(@cq_repo_path + "/lookout.json")
|
34
|
+
result = ERB.new(template).result
|
35
|
+
JSON.parse(result)
|
36
|
+
end
|
37
|
+
|
38
|
+
AemLookout::Watcher.run(@cq_repo_path, config)
|
data/example_config.json
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
// You can use ERB tags in this!
|
2
|
+
{
|
3
|
+
// array of CQ/AEM dev instance hostnames with HTTP basic auth and port
|
4
|
+
"instances": [
|
5
|
+
"http://admin:<%= ENV["CQ_PASSWORD"] %>@localhost:4502"
|
6
|
+
],
|
7
|
+
// array of relative paths to vault package roots (where jcr_root and META-INF live)
|
8
|
+
"jcrRootPaths": [
|
9
|
+
"jcr_content/src/content/jcr_root"
|
10
|
+
],
|
11
|
+
// array of objects specifying filesystem to jcr path mappings. Haven't finished this yet.
|
12
|
+
"slingInitialContentPaths": [
|
13
|
+
{"filesystem": "java-core/src/main/resources/components", "jcr": "/apps/clf/components"},
|
14
|
+
{"filesystem": "java-core/src/main/resources/clientlibs", "jcr": "/etc/designs/clf/clientlibs"}
|
15
|
+
],
|
16
|
+
// generic commands to do something when a specific
|
17
|
+
"commands": [
|
18
|
+
// watch a bundle for changes to classes and install to local instance when it happens
|
19
|
+
{"watch": "java-core/src/main/java", "pwd": "java-core", "command": "mvn install -P author-localhost"}
|
20
|
+
]
|
21
|
+
}
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'builder'
|
3
|
+
|
4
|
+
module AemLookout
|
5
|
+
class SlingInitialContentConverter
|
6
|
+
def self.convert(json_string)
|
7
|
+
node_data = JSON.parse(json_string)
|
8
|
+
builder = Builder::XmlMarkup.new
|
9
|
+
serialized_attributes, children = group_data(node_data)
|
10
|
+
|
11
|
+
builder.tag!("jcr:root", namespaces.merge(serialized_attributes)) do |b|
|
12
|
+
add_children(children, b)
|
13
|
+
end
|
14
|
+
rescue JSON::ParserError => e
|
15
|
+
raise SlingInitialContentConverterError, "A problem occurred while parsing JSON descriptor file: #{e.message}"
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.convert_package(package_path)
|
19
|
+
json_files = json_files_within(package_path)
|
20
|
+
json_files.each do |json_file|
|
21
|
+
content_xml_path = generate_content_xml_path(json_file)
|
22
|
+
FileUtils.mkdir_p(content_xml_path.parent)
|
23
|
+
xml_string = convert(File.read(json_file))
|
24
|
+
File.open(content_xml_path, 'w') {|f| f.write(xml_string) }
|
25
|
+
FileUtils.rm(json_file)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.json_files_within(path)
|
30
|
+
glob_path = Pathname(path).to_s + "/**/*"
|
31
|
+
Dir.glob(glob_path.to_s).delete_if {|path| !path.match(/\.json$/) }
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.generate_content_xml_path(json_path)
|
35
|
+
raise ArgumentError, "#{json_path} must end in .json" unless json_path.end_with?(".json")
|
36
|
+
node_name = File.basename(json_path, ".json")
|
37
|
+
Pathname(json_path).parent + node_name + ".content.xml"
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.add_children(children, builder)
|
41
|
+
children.each do |name, node_data|
|
42
|
+
serialized_attributes, children = group_data(node_data)
|
43
|
+
builder.tag!(name, serialized_attributes) do |b|
|
44
|
+
add_children(children, b)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Divide node data into serialized attributes and children
|
50
|
+
def self.group_data(node_data)
|
51
|
+
serialized_attributes = {}
|
52
|
+
children = {}
|
53
|
+
node_data.each do |key,value|
|
54
|
+
if !value.is_a?(Hash)
|
55
|
+
serialized_attributes[key] = convert_to_serialized_jcr_value(value)
|
56
|
+
else
|
57
|
+
children[key] = value
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
[default_attributes.merge(serialized_attributes), children]
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.default_attributes
|
65
|
+
{
|
66
|
+
"jcr:primaryType" => "nt:unstructured"
|
67
|
+
}
|
68
|
+
end
|
69
|
+
|
70
|
+
# All known namespaces
|
71
|
+
def self.namespaces
|
72
|
+
{
|
73
|
+
"xmlns:cq" => "http://www.day.com/jcr/cq/1.0",
|
74
|
+
"xmlns:sling" => "http://sling.apache.org/jcr/sling/1.0",
|
75
|
+
"xmlns:jcr" => "http://www.jcp.org/jcr/1.0",
|
76
|
+
"xmlns:vlt" => "http://www.day.com/jcr/vault/1.0",
|
77
|
+
"xmlns:nt" => "http://www.jcp.org/jcr/nt/1.0"
|
78
|
+
}
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.convert_to_serialized_jcr_value(value)
|
82
|
+
if value == true || value == false
|
83
|
+
"{Boolean}#{value}"
|
84
|
+
elsif value.is_a?(Array)
|
85
|
+
values = value.map {|el| convert_to_serialized_jcr_value(el) }
|
86
|
+
"[#{values.join(",")}]"
|
87
|
+
elsif value.is_a?(Time)
|
88
|
+
"{Date}#{value.strftime("%Y-%m-%dT%H:%M:%S.%L%:z")}"
|
89
|
+
elsif value.is_a?(String)
|
90
|
+
value
|
91
|
+
else
|
92
|
+
raise "Unknown type, cannot serialize #{value.class} value: #{value}"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
class SlingInitialContentConverterError < LookoutError; end
|
98
|
+
end
|
@@ -0,0 +1,186 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'pathname'
|
3
|
+
require 'erb'
|
4
|
+
require 'tmpdir'
|
5
|
+
require 'logger'
|
6
|
+
|
7
|
+
module AemLookout
|
8
|
+
class Sync
|
9
|
+
class Hostname
|
10
|
+
attr_accessor :url
|
11
|
+
|
12
|
+
def initialize(hostname)
|
13
|
+
@url = URI.parse(hostname)
|
14
|
+
end
|
15
|
+
|
16
|
+
def url_without_credentials
|
17
|
+
"#{url.scheme}://#{url.host}:#{url.port}/crx/-/jcr:root"
|
18
|
+
end
|
19
|
+
|
20
|
+
def credentials
|
21
|
+
"#{url.user}:#{url.password}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.run(options)
|
26
|
+
self.new(options).run
|
27
|
+
end
|
28
|
+
|
29
|
+
attr_accessor :log, :hostnames, :filesystem_path, :jcr_path
|
30
|
+
|
31
|
+
def initialize(options)
|
32
|
+
default_log = Logger.new(options.fetch(:output, STDOUT))
|
33
|
+
@log = options.fetch(:log, default_log)
|
34
|
+
@hostnames = options.fetch(:hostnames).map {|hostname| Hostname.new(hostname) }
|
35
|
+
@sling_initial_content = options.fetch(:sling_initial_content, false)
|
36
|
+
@filesystem_path = options.fetch(:filesystem)
|
37
|
+
@jcr_path = options.fetch(:jcr)
|
38
|
+
rescue KeyError => e
|
39
|
+
raise ArgumentError.new("#{e.message} (missing a hash argument)")
|
40
|
+
end
|
41
|
+
|
42
|
+
# Locals on the local filesystem to copy into the package
|
43
|
+
def content_paths
|
44
|
+
if @content_paths.nil?
|
45
|
+
paths = if filesystem_path.end_with?(".content.xml")
|
46
|
+
Pathname(filesystem_path).parent.to_s
|
47
|
+
elsif sling_initial_content? and filesystem_path.end_with?(".json")
|
48
|
+
[filesystem_path, filesystem_path.gsub(/.json$/, "")].delete_if { |path| !File.exist?(path) }
|
49
|
+
else
|
50
|
+
filesystem_path
|
51
|
+
end
|
52
|
+
|
53
|
+
@content_paths = Array(paths)
|
54
|
+
end
|
55
|
+
|
56
|
+
@content_paths
|
57
|
+
end
|
58
|
+
|
59
|
+
# Paths in the package to install into the JCR
|
60
|
+
def filter_paths
|
61
|
+
if @filter_paths.nil?
|
62
|
+
paths = if jcr_path.end_with?(".content.xml")
|
63
|
+
Pathname(jcr_path).parent.to_s
|
64
|
+
elsif sling_initial_content? and jcr_path.end_with?(".json")
|
65
|
+
jcr_path.gsub(/.json$/, "")
|
66
|
+
else
|
67
|
+
jcr_path
|
68
|
+
end
|
69
|
+
|
70
|
+
@filter_paths = Array(paths)
|
71
|
+
end
|
72
|
+
|
73
|
+
@filter_paths
|
74
|
+
end
|
75
|
+
|
76
|
+
def run
|
77
|
+
start_timer
|
78
|
+
build_package
|
79
|
+
install_package
|
80
|
+
log_elapsed_time
|
81
|
+
end
|
82
|
+
|
83
|
+
def start_timer
|
84
|
+
@start_time = Time.now
|
85
|
+
end
|
86
|
+
|
87
|
+
def log_elapsed_time
|
88
|
+
elapsed_time = (Time.now.to_f - @start_time.to_f).round(2)
|
89
|
+
log.info "Elapsed time: #{elapsed_time} seconds"
|
90
|
+
end
|
91
|
+
|
92
|
+
def build_package
|
93
|
+
copy_content_to_package
|
94
|
+
create_settings
|
95
|
+
create_filter
|
96
|
+
SlingInitialContentConverter.convert_package(package_path) if sling_initial_content?
|
97
|
+
sleep 0.1 # just in case
|
98
|
+
end
|
99
|
+
|
100
|
+
def copy_content_to_package
|
101
|
+
FileUtils.mkdir_p(target_content_path_root)
|
102
|
+
content_paths.each do |content_path|
|
103
|
+
log.info "Copying content from #{content_path} to target content path root"
|
104
|
+
FileUtils.cp_r(content_path, target_content_path_root)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def create_settings
|
109
|
+
FileUtils.mkdir_p(vault_config_path)
|
110
|
+
File.open(vault_config_path + "settings.xml", 'w') {|f| f.write(settings_template) }
|
111
|
+
end
|
112
|
+
|
113
|
+
def create_filter
|
114
|
+
File.open(vault_config_path + "filter.xml", 'w') do |f|
|
115
|
+
paths = filter_paths
|
116
|
+
f.write(ERB.new(filter_template).result(binding))
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def sling_initial_content_filter_paths_for(jcr_path)
|
121
|
+
|
122
|
+
end
|
123
|
+
|
124
|
+
def install_package
|
125
|
+
hostnames.map do |hostname|
|
126
|
+
install_package_to_hostname(hostname)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def install_package_multithreaded
|
131
|
+
threads = hostnames.map do |hostname|
|
132
|
+
Thread.new do
|
133
|
+
install_package_to_hostname(hostname)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
threads.each {|thread| thread.join } # wait for threads
|
138
|
+
end
|
139
|
+
|
140
|
+
def install_package_to_hostname(hostname)
|
141
|
+
log.info "Installing package at #{package_path} to #{hostname.url_without_credentials}"
|
142
|
+
command = "#{AemLookout.vlt_executable} --credentials #{hostname.credentials} -v import #{hostname.url_without_credentials} #{package_path} /"
|
143
|
+
Terminal.new(log).execute_command(command)
|
144
|
+
end
|
145
|
+
|
146
|
+
def vault_config_path
|
147
|
+
package_path + "META-INF/vault"
|
148
|
+
end
|
149
|
+
|
150
|
+
def target_content_path_root
|
151
|
+
(jcr_root_path + jcr_path.gsub(/^\//, "")).parent
|
152
|
+
end
|
153
|
+
|
154
|
+
def package_path
|
155
|
+
@package_path ||= Pathname(Dir.mktmpdir("vlt-sync"))
|
156
|
+
end
|
157
|
+
|
158
|
+
def jcr_root_path
|
159
|
+
@jcr_root ||= package_path + "jcr_root"
|
160
|
+
end
|
161
|
+
|
162
|
+
def sling_initial_content?
|
163
|
+
@sling_initial_content
|
164
|
+
end
|
165
|
+
|
166
|
+
def settings_template
|
167
|
+
<<-EOF
|
168
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
169
|
+
<vault version="0.1">
|
170
|
+
<ignore name=".svn"/>
|
171
|
+
<ignore name=".gitignore"/>
|
172
|
+
<ignore name=".DS_Store"/>
|
173
|
+
</vault>
|
174
|
+
EOF
|
175
|
+
end
|
176
|
+
|
177
|
+
def filter_template
|
178
|
+
<<-EOF
|
179
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
180
|
+
<workspaceFilter vesion="0.1">
|
181
|
+
<% paths.each do |path| %> <filter root="<%= path %>" mode="replace"/>\n<% end %>
|
182
|
+
</workspaceFilter>
|
183
|
+
EOF
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'open3'
|
2
|
+
|
3
|
+
# A helper class for running terminal commands in the background while
|
4
|
+
# streaming to a Ruby logger.
|
5
|
+
module AemLookout
|
6
|
+
class Terminal
|
7
|
+
attr_reader :log
|
8
|
+
|
9
|
+
def initialize(log = Logger.new(STDOUT))
|
10
|
+
@log = log
|
11
|
+
end
|
12
|
+
|
13
|
+
def execute_command(command)
|
14
|
+
Open3.popen3(command) do |stdin, stdout, stderr, thread|
|
15
|
+
flush(stdout: stdout, stderr: stderr) until !thread.alive?
|
16
|
+
flush(stdout: stdout, stderr: stderr)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def flush(options = {})
|
21
|
+
stdout_thread = stream_to_log(options.fetch(:stdout))
|
22
|
+
stderr_thread = stream_to_log(options.fetch(:stderr), error: true)
|
23
|
+
sleep 0.1 until !stdout_thread.alive? and !stdout_thread.alive?
|
24
|
+
end
|
25
|
+
|
26
|
+
def stream_to_log(io, options = {})
|
27
|
+
options = options.merge({error: false})
|
28
|
+
thread = Thread.new do
|
29
|
+
while message = io.gets
|
30
|
+
if options.fetch(:error)
|
31
|
+
log.error message.chomp
|
32
|
+
else
|
33
|
+
log.info message.chomp
|
34
|
+
end
|
35
|
+
|
36
|
+
sleep 0.01 # to ensure line isn't still being written to
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,223 @@
|
|
1
|
+
module AemLookout
|
2
|
+
class Watcher
|
3
|
+
def self.run(repo_path, config)
|
4
|
+
self.new(repo_path, config).run
|
5
|
+
end
|
6
|
+
|
7
|
+
attr_accessor :repo_path, :config, :log
|
8
|
+
|
9
|
+
def initialize(repo_path, config, log = nil)
|
10
|
+
@repo_path = repo_path
|
11
|
+
@config = config
|
12
|
+
@log = log || Logger.new(STDOUT)
|
13
|
+
end
|
14
|
+
|
15
|
+
def run
|
16
|
+
threads = jcr_root_paths.map do |jcr_root_paths|
|
17
|
+
Thread.new { watch_vault_package(jcr_root_paths) }
|
18
|
+
end
|
19
|
+
|
20
|
+
threads += sling_initial_content_paths.map do |sling_initial_content_path|
|
21
|
+
Thread.new { watch_sling_initial_content(sling_initial_content_path) }
|
22
|
+
end
|
23
|
+
|
24
|
+
threads += command_configs.map do |command_config|
|
25
|
+
Thread.new { watch_command_config(command_config) }
|
26
|
+
end
|
27
|
+
|
28
|
+
wait_for(threads)
|
29
|
+
end
|
30
|
+
|
31
|
+
def wait_for(threads)
|
32
|
+
sleep 1 until Array(threads).map {|thread| thread.join(1) }.all? {|result| result }
|
33
|
+
end
|
34
|
+
|
35
|
+
def watch_vault_package(jcr_root)
|
36
|
+
if !File.exist?(jcr_root)
|
37
|
+
log.warn "jcr_root points to non-existing directory: #{jcr_root}"
|
38
|
+
return
|
39
|
+
end
|
40
|
+
|
41
|
+
options = {:latency => 0.1, :file_events => true}
|
42
|
+
fsevent = create_threaded_fsevent jcr_root.to_s, options do |paths|
|
43
|
+
sync_vault_package_paths(paths)
|
44
|
+
end
|
45
|
+
|
46
|
+
log.info "Watching jcr_root at #{jcr_root} for changes..."
|
47
|
+
fsevent.run
|
48
|
+
end
|
49
|
+
|
50
|
+
# Watch a given path with speified options and call action_block when a
|
51
|
+
# non-ignored path is modified. This also ensures that only one
|
52
|
+
# action_block is running at a time, killing any other running
|
53
|
+
# blocks before starting a new one.
|
54
|
+
def create_threaded_fsevent(watch_path, options, &action_block)
|
55
|
+
fsevent = FSEvent.new
|
56
|
+
running_jobs = Set.new
|
57
|
+
|
58
|
+
fsevent.watch watch_path, options do |paths|
|
59
|
+
paths.delete_if {|path| ignored?(path) }
|
60
|
+
log.warn "Detected change inside: #{paths.inspect}" unless paths.empty?
|
61
|
+
|
62
|
+
if running_jobs.length > 0
|
63
|
+
log.warn "A job is currently running for this watcher, killing..."
|
64
|
+
running_jobs.each {|thread| thread.kill }
|
65
|
+
else
|
66
|
+
log.warn "Phew, no running jobs: #{running_jobs}"
|
67
|
+
end
|
68
|
+
|
69
|
+
job = Thread.new do
|
70
|
+
action_block.call(paths)
|
71
|
+
Thread.exit
|
72
|
+
end
|
73
|
+
|
74
|
+
track_job_on_list(job, running_jobs)
|
75
|
+
end
|
76
|
+
|
77
|
+
fsevent
|
78
|
+
end
|
79
|
+
|
80
|
+
# Adds to running job list and removes from list when thread completes.
|
81
|
+
def track_job_on_list(job, running_jobs)
|
82
|
+
Thread.new do
|
83
|
+
running_jobs << job
|
84
|
+
log.warn "Waiting for #{job} to finish"
|
85
|
+
wait_for(job)
|
86
|
+
log.warn "#{job} job finished"
|
87
|
+
running_jobs.delete(job)
|
88
|
+
Thread.exit
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def sync_vault_package_paths(paths)
|
93
|
+
paths.each do |path|
|
94
|
+
if !File.exist?(path)
|
95
|
+
log.warn "#{path} no longer exists, syncing parent instead"
|
96
|
+
path = File.dirname(path)
|
97
|
+
end
|
98
|
+
|
99
|
+
jcr_path = discover_jcr_path_from_file_in_vault_package(path)
|
100
|
+
|
101
|
+
AemLookout::Sync.new(
|
102
|
+
hostnames: hostnames,
|
103
|
+
filesystem: path,
|
104
|
+
jcr: jcr_path,
|
105
|
+
log: log
|
106
|
+
).run
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def watch_sling_initial_content(path)
|
111
|
+
filesystem_path = path.fetch("filesystem")
|
112
|
+
jcr_path = path.fetch("jcr")
|
113
|
+
|
114
|
+
if !File.exist?(filesystem_path)
|
115
|
+
log.warn "Filesystem path for Sling-Initial-Content points to non-existing directory: #{filesystem_path}"
|
116
|
+
return
|
117
|
+
end
|
118
|
+
|
119
|
+
options = {:latency => 0.1, :file_events => true}
|
120
|
+
fsevent = create_threaded_fsevent filesystem_path.to_s, options do |paths|
|
121
|
+
begin
|
122
|
+
handle_sling_initial_content_change(paths, filesystem_path, jcr_path)
|
123
|
+
rescue LookoutError => e
|
124
|
+
log.error "An error occurred while handling sling initial content change: #{e.message}"
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
log.info "Watching Sling-Initial-Content at #{filesystem_path} for changes..."
|
129
|
+
fsevent.run
|
130
|
+
end
|
131
|
+
|
132
|
+
def handle_sling_initial_content_change(paths, filesystem_path, jcr_path)
|
133
|
+
paths.each do |path|
|
134
|
+
if !File.exist?(path)
|
135
|
+
log.info "#{path} no longer exists, syncing parent instead"
|
136
|
+
path = File.dirname(path)
|
137
|
+
end
|
138
|
+
|
139
|
+
relative_jcr_path = path.gsub(/^.+#{filesystem_path}\//, "")
|
140
|
+
|
141
|
+
AemLookout::Sync.new(
|
142
|
+
hostnames: hostnames,
|
143
|
+
filesystem: path,
|
144
|
+
jcr: (Pathname(jcr_path) + relative_jcr_path).to_s,
|
145
|
+
log: log,
|
146
|
+
sling_initial_content: true
|
147
|
+
).run
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def watch_command_config(command_config)
|
152
|
+
watch_path = command_config.fetch("watch")
|
153
|
+
pwd = Pathname(repo_path) + command_config.fetch("pwd", "")
|
154
|
+
command = command_config.fetch("command")
|
155
|
+
|
156
|
+
options = {:latency => 1, :file_events => true}
|
157
|
+
fsevent = create_threaded_fsevent watch_path, options do |paths|
|
158
|
+
break if paths.empty?
|
159
|
+
log.info "Running command"
|
160
|
+
Terminal.new(log).execute_command("cd #{pwd} && #{command}")
|
161
|
+
end
|
162
|
+
|
163
|
+
log.info "Watching #{watch_path}, changes will run #{command.inspect}..."
|
164
|
+
fsevent.run
|
165
|
+
end
|
166
|
+
|
167
|
+
def jcr_root_paths
|
168
|
+
config.fetch("jcrRootPaths", []).map {|jcr_root_path| Pathname(repo_path) + jcr_root_path }.map(&:to_s)
|
169
|
+
end
|
170
|
+
|
171
|
+
def sling_initial_content_paths
|
172
|
+
config.fetch("slingInitialContentPaths", []).map do |sling_initial_content_path|
|
173
|
+
validate_sling_initial_content_path!(sling_initial_content_path)
|
174
|
+
sling_initial_content_path
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
# Something like this [{"watch" => "java-core/src/main/java", "pwd" => "java-core", "command" => "mvn install -P author-localhost"}]
|
179
|
+
def command_configs
|
180
|
+
config.fetch("commands", []).map do |command_config|
|
181
|
+
validate_command_config!(command_config)
|
182
|
+
command_config
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
# Ensures required keys are present. This is ugly.
|
187
|
+
def validate_command_config!(command_config)
|
188
|
+
required_keys = ["watch", "command"]
|
189
|
+
unless command_config.has_key?(required_keys.first) and command_config.has_key?(required_keys.last)
|
190
|
+
raise "commands entry is malformed (requires these keys: #{required_keys.join(", ")}): #{command_config.inspect}"
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def validate_sling_initial_content_path!(path)
|
195
|
+
unless path.has_key?("filesystem") and path.has_key?("jcr")
|
196
|
+
raise "slingInitialContentPaths entry is malformed (requires \"filesystem\" and \"jcr\" entry): #{path.inspect}"
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def hostnames
|
201
|
+
config.fetch("instances")
|
202
|
+
end
|
203
|
+
|
204
|
+
# Return true if file should not trigger a sync
|
205
|
+
def ignored?(file)
|
206
|
+
return true if File.extname(file) == ".tmp"
|
207
|
+
return true if file.match(/___$/)
|
208
|
+
return true if File.basename(file) == ".DS_Store"
|
209
|
+
return false
|
210
|
+
end
|
211
|
+
|
212
|
+
# Find the root of the package to determine the path used for the filter
|
213
|
+
def discover_jcr_path_from_file_in_vault_package(filesystem_path)
|
214
|
+
possible_jcr_root = Pathname(filesystem_path).parent
|
215
|
+
while !possible_jcr_root.root?
|
216
|
+
break if possible_jcr_root.basename.to_s == "jcr_root"
|
217
|
+
possible_jcr_root = possible_jcr_root.parent
|
218
|
+
end
|
219
|
+
|
220
|
+
filesystem_path.gsub(/^#{possible_jcr_root.to_s}/, "")
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
data/lib/aem_lookout.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'vlt_wrapper'
|
2
|
+
|
3
|
+
require 'aem_lookout/version'
|
4
|
+
require 'aem_lookout/lookout_error'
|
5
|
+
require 'aem_lookout/sling_initial_content_converter'
|
6
|
+
require 'aem_lookout/sync'
|
7
|
+
require 'aem_lookout/terminal'
|
8
|
+
require 'aem_lookout/watcher'
|
9
|
+
|
10
|
+
|
11
|
+
module AemLookout
|
12
|
+
def vlt_executable
|
13
|
+
VltWrapper.executable
|
14
|
+
end
|
15
|
+
|
16
|
+
extend self
|
17
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe AemLookout::SlingInitialContentConverter do
|
4
|
+
it "converts a json file into a .content.xml file" do
|
5
|
+
node_example = {
|
6
|
+
"cq:isContainer" => false,
|
7
|
+
"jcr:description" => "A list of upcoming events pulled from a calendar",
|
8
|
+
"jcr:primaryType" => "cq:Component",
|
9
|
+
"jcr:title" => "Upcoming Events",
|
10
|
+
"componentGroup" => "Social"
|
11
|
+
}
|
12
|
+
content_xml = AemLookout::SlingInitialContentConverter.convert(node_example.to_json)
|
13
|
+
xml_exemplar = '<jcr:root xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:vlt="http://www.day.com/jcr/vault/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0" cq:isContainer="{Boolean}false" jcr:description="A list of upcoming events pulled from a calendar" jcr:primaryType="cq:Component" jcr:title="Upcoming Events" componentGroup="Social"></jcr:root>'
|
14
|
+
expect(content_xml).to eq(xml_exemplar)
|
15
|
+
end
|
16
|
+
|
17
|
+
# xit "prints an example of a complex JSON desciptor file" do
|
18
|
+
# json = File.read("/Users/jnraine/projects/cq/java-core/src/main/resources/components/twitter-timeline.json")
|
19
|
+
# puts AemLookout::SlingInitialContentConverter.convert(json)
|
20
|
+
# end
|
21
|
+
|
22
|
+
describe ".convert_to_serialized_jcr_value" do
|
23
|
+
let(:klass) { AemLookout::SlingInitialContentConverter }
|
24
|
+
|
25
|
+
it "converts booleans to serialized JCR booleans" do
|
26
|
+
klass.convert_to_serialized_jcr_value(true).should == "{Boolean}true"
|
27
|
+
klass.convert_to_serialized_jcr_value(false).should == "{Boolean}false"
|
28
|
+
end
|
29
|
+
|
30
|
+
it "converts arrays to serialized JCR arrays" do
|
31
|
+
klass.convert_to_serialized_jcr_value(["foo", "bar", "baz"]).should == "[foo,bar,baz]"
|
32
|
+
end
|
33
|
+
|
34
|
+
it "converts dates to serialized JCR dates" do
|
35
|
+
# this only handles local time
|
36
|
+
local_time = Time.parse("2010-03-17T17:14:41.775")
|
37
|
+
klass.convert_to_serialized_jcr_value(local_time).should == "{Date}2010-03-17T17:14:41.775-07:00"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
describe "#convert_json_descriptor_files" do
|
43
|
+
def build_fake_package
|
44
|
+
tmp_dir = Pathname(Dir.mktmpdir("sync-test"))
|
45
|
+
File.open(tmp_dir + "#{node_name}.json", 'w') {|f| f.write(properties.to_json) }
|
46
|
+
tmp_dir
|
47
|
+
end
|
48
|
+
|
49
|
+
let(:properties) do
|
50
|
+
{foo: "hello", bar: "world", baz: "ur nice"}
|
51
|
+
end
|
52
|
+
|
53
|
+
let(:node_name) { "foo" }
|
54
|
+
let(:fake_package_path) { build_fake_package }
|
55
|
+
|
56
|
+
it "takes any json files at a given path and converts them to .content.xml files" do
|
57
|
+
AemLookout::SlingInitialContentConverter.convert_package(fake_package_path)
|
58
|
+
content_xml_path = fake_package_path + "foo" + ".content.xml"
|
59
|
+
expect(File).to exist(content_xml_path)
|
60
|
+
expect(File.read(content_xml_path)).to match("foo=\"hello\" bar=\"world\" baz=\"ur nice\"")
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
describe ".json_files_within" do
|
65
|
+
it "returns all json files found within a directory structure recursively" do
|
66
|
+
# setup
|
67
|
+
tmp_dir = Pathname(Dir.mktmpdir("test"))
|
68
|
+
nested_dir = tmp_dir + "foo" + "bar" + "baz"
|
69
|
+
FileUtils.mkdir_p(nested_dir.to_s)
|
70
|
+
sleep 0.01
|
71
|
+
FileUtils.touch([tmp_dir + "my.json", nested_dir + "another.json"])
|
72
|
+
# execute and assert
|
73
|
+
json_files = AemLookout::SlingInitialContentConverter.json_files_within(tmp_dir)
|
74
|
+
expect(json_files.length).to eq(2)
|
75
|
+
json_files.each {|json_file| expect(json_file).to end_with(".json") }
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
describe ".generate_content_xml_path" do
|
80
|
+
it "creates a content XML path for the named node" do
|
81
|
+
content_xml_path = AemLookout::SlingInitialContentConverter.generate_content_xml_path("/foo/my_node.json")
|
82
|
+
expect(content_xml_path).to eq(Pathname("/foo/my_node/.content.xml"))
|
83
|
+
end
|
84
|
+
|
85
|
+
it "raises an error when passed a path to a non-json file" do
|
86
|
+
expect {
|
87
|
+
AemLookout::SlingInitialContentConverter.generate_content_xml_path("/foo/my_node.txt")
|
88
|
+
}.to raise_error(ArgumentError)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
$: << File.expand_path("..", File.dirname(__FILE__))
|
2
|
+
require 'aem_lookout'
|
3
|
+
|
4
|
+
RSpec.configure do |config|
|
5
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
6
|
+
config.run_all_when_everything_filtered = true
|
7
|
+
config.filter_run :focus
|
8
|
+
|
9
|
+
config.order = 'random'
|
10
|
+
end
|
data/sync.sh
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
VLT=/Users/jnraine/projects/cq/instances/author/crx-quickstart/opt/filevault/vault-cli-2.4.18/bin/vlt
|
4
|
+
WHERE_JCR_ROOT_AND_META_INF_IS=/tmp/playground
|
5
|
+
JCR_ROOT_PATH=/
|
6
|
+
CQ_HOST="http://localhost:4502"
|
7
|
+
CREDENTIALS="admin:cq4me"
|
8
|
+
|
9
|
+
$VLT --credentials $CREDENTIALS -v import $CQ_HOST/crx/-/jcr:root $WHERE_JCR_ROOT_AND_META_INF_IS $JCR_ROOT_PATH
|
metadata
ADDED
@@ -0,0 +1,154 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: aem_lookout
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jordan Raine
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2014-03-03 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: vlt_wrapper
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 2.4.18
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 2.4.18
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: builder
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 3.2.2
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 3.2.2
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rb-fsevent
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 0.9.4
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.9.4
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: bundler
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ~>
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '1.5'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ~>
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '1.5'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: rspec
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ~>
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: 2.14.1
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ~>
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: 2.14.1
|
94
|
+
description:
|
95
|
+
email:
|
96
|
+
- jnraine@gmail.com
|
97
|
+
executables:
|
98
|
+
- aem_lookout
|
99
|
+
- lookout
|
100
|
+
extensions: []
|
101
|
+
extra_rdoc_files: []
|
102
|
+
files:
|
103
|
+
- Gemfile
|
104
|
+
- Gemfile.lock
|
105
|
+
- LICENSE.txt
|
106
|
+
- README.md
|
107
|
+
- Rakefile
|
108
|
+
- aem_lookout.gemspec
|
109
|
+
- bin/aem_lookout
|
110
|
+
- bin/lookout
|
111
|
+
- example_config.json
|
112
|
+
- lib/aem_lookout.rb
|
113
|
+
- lib/aem_lookout/lookout_error.rb
|
114
|
+
- lib/aem_lookout/sling_initial_content_converter.rb
|
115
|
+
- lib/aem_lookout/sync.rb
|
116
|
+
- lib/aem_lookout/terminal.rb
|
117
|
+
- lib/aem_lookout/version.rb
|
118
|
+
- lib/aem_lookout/watcher.rb
|
119
|
+
- spec/lib/aem_lookout/sling_initial_content_converter_spec.rb
|
120
|
+
- spec/lib/aem_lookout/sync_spec.rb
|
121
|
+
- spec/lib/aem_lookout_spec.rb
|
122
|
+
- spec/spec_helper.rb
|
123
|
+
- sync.sh
|
124
|
+
homepage: ''
|
125
|
+
licenses:
|
126
|
+
- MIT
|
127
|
+
post_install_message:
|
128
|
+
rdoc_options: []
|
129
|
+
require_paths:
|
130
|
+
- lib
|
131
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
132
|
+
none: false
|
133
|
+
requirements:
|
134
|
+
- - ! '>='
|
135
|
+
- !ruby/object:Gem::Version
|
136
|
+
version: '0'
|
137
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
138
|
+
none: false
|
139
|
+
requirements:
|
140
|
+
- - ! '>='
|
141
|
+
- !ruby/object:Gem::Version
|
142
|
+
version: '0'
|
143
|
+
requirements: []
|
144
|
+
rubyforge_project:
|
145
|
+
rubygems_version: 1.8.23
|
146
|
+
signing_key:
|
147
|
+
specification_version: 3
|
148
|
+
summary: Speeds up iteration loop while developing for AEM/CQ.
|
149
|
+
test_files:
|
150
|
+
- spec/lib/aem_lookout/sling_initial_content_converter_spec.rb
|
151
|
+
- spec/lib/aem_lookout/sync_spec.rb
|
152
|
+
- spec/lib/aem_lookout_spec.rb
|
153
|
+
- spec/spec_helper.rb
|
154
|
+
has_rdoc:
|