pmgmt 1.0.0
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 +7 -0
- data/lib/dockerhelper.rb +47 -0
- data/lib/pmgmt.rb +218 -0
- data/lib/syncfiles.rb +205 -0
- metadata +46 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 3e30d7e78a8a61ae27c505f47f1821c2ee991ac91264abfed389651626823e9a
|
|
4
|
+
data.tar.gz: 151734accba09a0511a88d78380894de6c4af6fad2ad0613c701441eb89963de
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 764187c3dcb66e45fd17b6e111d8230f06e4fa622f75690f9cf3a43639f75777392b1d8a37cbb59d6b13e854da3e2411e5c3e3e2fe5d6c1c5ad28fe20c1cbfb2
|
|
7
|
+
data.tar.gz: 97a9f031cd58f3ba87e97104cef2be3945a27c13815a55be444507cf9184f7285a3e6c011ee3e44d62fd0a7d5d586f31cd0ff817ed03373509f00ef9043f4fea
|
data/lib/dockerhelper.rb
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module PmgmtLib
|
|
2
|
+
class DockerHelper
|
|
3
|
+
attr :c
|
|
4
|
+
|
|
5
|
+
def initialize(common)
|
|
6
|
+
@c = common
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def in_docker?()
|
|
10
|
+
File.exist?("/.dockerenv")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def requires_docker()
|
|
14
|
+
status = c.run %W{which docker}
|
|
15
|
+
unless status.success?
|
|
16
|
+
c.error "docker not installed."
|
|
17
|
+
STDERR.puts "Installation instructions:"
|
|
18
|
+
STDERR.puts "\n https://www.docker.com/community-edition\n\n"
|
|
19
|
+
exit 1
|
|
20
|
+
end
|
|
21
|
+
status = c.run %W{docker info}
|
|
22
|
+
unless status.success?
|
|
23
|
+
c.error "`docker info` command failed."
|
|
24
|
+
STDERR.puts "This is usually a permissions problem. Try allowing your user to run docker\n"
|
|
25
|
+
STDERR.puts "without sudo:"
|
|
26
|
+
STDERR.puts "\n$ sudo usermod -aG docker #{ENV["USER"]}\n\n"
|
|
27
|
+
c.error "Note: You will need to log-in to a new shell before this change will take effect.\n"
|
|
28
|
+
exit 1
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def image_exists?(name)
|
|
33
|
+
requires_docker
|
|
34
|
+
fmt = "{{.Repository}}:{{.Tag}}"
|
|
35
|
+
c.capture_stdout(%W{docker images --format #{fmt}}).include?(name)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def ensure_image(name)
|
|
39
|
+
requires_docker
|
|
40
|
+
if not image_exists?(name)
|
|
41
|
+
c.error "Missing docker image \"#{name}\". Pulling..."
|
|
42
|
+
c.run_inline(%W{docker pull #{name}})
|
|
43
|
+
c.status "Image \"#{name}\" pulled."
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
data/lib/pmgmt.rb
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
require "open3"
|
|
2
|
+
require "ostruct"
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
require_relative "dockerhelper"
|
|
6
|
+
require_relative "syncfiles"
|
|
7
|
+
|
|
8
|
+
class Pmgmt
|
|
9
|
+
@@commands = []
|
|
10
|
+
|
|
11
|
+
def self.load_scripts(scripts_dir)
|
|
12
|
+
if !File.directory?(scripts_dir)
|
|
13
|
+
self.new.error "Cannot load scripts. Not a directory: #{scripts_dir}"
|
|
14
|
+
exit 1
|
|
15
|
+
end
|
|
16
|
+
Dir.foreach(scripts_dir) do |item|
|
|
17
|
+
if item =~ /[.]rb$/
|
|
18
|
+
require "#{scripts_dir}/#{item}"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.register_command(command)
|
|
24
|
+
invocation = command[:invocation]
|
|
25
|
+
fn = command[:fn]
|
|
26
|
+
if fn.nil?
|
|
27
|
+
self.new.error "No :fn key defined for command #{invocation}"
|
|
28
|
+
exit 1
|
|
29
|
+
end
|
|
30
|
+
if fn.is_a?(Symbol)
|
|
31
|
+
unless Object.private_method_defined?(fn)
|
|
32
|
+
self.new.error "Function #{fn.to_s} is not defined for #{invocation}."
|
|
33
|
+
exit 1
|
|
34
|
+
end
|
|
35
|
+
else
|
|
36
|
+
# TODO(dmohs): Deprecation warning.
|
|
37
|
+
unless fn.is_a?(Proc)
|
|
38
|
+
self.new.error ":fn key for #{invocation} does not define a Proc or Symbol"
|
|
39
|
+
exit 1
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
@@commands.push(command)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.commands()
|
|
47
|
+
@@commands
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.handle_or_die(args)
|
|
51
|
+
if args.length == 0 or args[0] == "--help"
|
|
52
|
+
self.new.print_usage
|
|
53
|
+
exit 0
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
if args[0] == "--cmplt" # Shell completion argument name inspired by vault
|
|
57
|
+
# Form of args: --cmplt <index-of-current-argument> ./project.rb arg arg arg
|
|
58
|
+
index = args[1].to_i
|
|
59
|
+
word = args[2 + index]
|
|
60
|
+
puts @@commands.select{ |x| x[:invocation].start_with?(word) }
|
|
61
|
+
.map{ |x| x[:invocation]}.join("\n")
|
|
62
|
+
exit 0
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
command = args.first
|
|
66
|
+
handler = @@commands.select{ |x| x[:invocation] == command }.first
|
|
67
|
+
if handler.nil?
|
|
68
|
+
error "#{command} command not found."
|
|
69
|
+
exit 1
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
fn = handler[:fn]
|
|
73
|
+
if fn.is_a?(Symbol)
|
|
74
|
+
method(fn).call(*args)
|
|
75
|
+
else
|
|
76
|
+
handler[:fn].call(*args.drop(1))
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
attr :docker
|
|
81
|
+
attr :sf
|
|
82
|
+
|
|
83
|
+
def initialize()
|
|
84
|
+
@docker = PmgmtLib::DockerHelper.new(self)
|
|
85
|
+
@sf = PmgmtLib::SyncFiles.new(self)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def print_usage()
|
|
89
|
+
STDERR.puts "\nUsage: #{$PROGRAM_NAME} <command> <options>\n\n"
|
|
90
|
+
if !@@commands.empty?
|
|
91
|
+
STDERR.puts "COMMANDS\n\n"
|
|
92
|
+
@@commands.each do |command|
|
|
93
|
+
STDERR.puts bold_term_text(command[:invocation])
|
|
94
|
+
STDERR.puts command[:description] || "[No description provided.]"
|
|
95
|
+
STDERR.puts
|
|
96
|
+
end
|
|
97
|
+
else
|
|
98
|
+
STDERR.puts " >> No commands defined.\n\n"
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def load_env()
|
|
103
|
+
if not File.exists?("project.yaml")
|
|
104
|
+
error "Missing project.yaml"
|
|
105
|
+
exit 1
|
|
106
|
+
end
|
|
107
|
+
OpenStruct.new YAML.load(File.read("project.yaml"))
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def red_term_text(text)
|
|
111
|
+
"\033[0;31m#{text}\033[0m"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def blue_term_text(text)
|
|
115
|
+
"\033[0;36m#{text}\033[0m"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def yellow_term_text(text)
|
|
119
|
+
"\033[0;33m#{text}\033[0m"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def bold_term_text(text)
|
|
123
|
+
"\033[1m#{text}\033[0m"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def status(text)
|
|
127
|
+
STDERR.puts blue_term_text(text)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def warning(text)
|
|
131
|
+
STDERR.puts yellow_term_text(text)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def error(text)
|
|
135
|
+
STDERR.puts red_term_text(text)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def put_command(cmd, redact=nil)
|
|
139
|
+
if cmd.is_a?(String)
|
|
140
|
+
command_string = "+ #{cmd}"
|
|
141
|
+
else
|
|
142
|
+
command_string = "+ #{cmd.join(" ")}"
|
|
143
|
+
end
|
|
144
|
+
command_to_echo = command_string.clone
|
|
145
|
+
if redact
|
|
146
|
+
command_to_echo.sub! redact, "*" * redact.length
|
|
147
|
+
end
|
|
148
|
+
STDERR.puts command_to_echo
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Pass err=nil to suppress stderr.
|
|
152
|
+
def capture_stdout(cmd, err = STDERR)
|
|
153
|
+
if err.nil?
|
|
154
|
+
err = "/dev/null"
|
|
155
|
+
end
|
|
156
|
+
output, _ = Open3.capture2(*cmd, :err => err)
|
|
157
|
+
output
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def run_inline(cmd, redact=nil)
|
|
161
|
+
put_command(cmd, redact)
|
|
162
|
+
|
|
163
|
+
# `system`, by design (?!), hides stderr when the command fails.
|
|
164
|
+
if ENV["PROJECTRB_USE_SYSTEM"] == "true"
|
|
165
|
+
if not system(*cmd)
|
|
166
|
+
exit $?.exitstatus
|
|
167
|
+
end
|
|
168
|
+
else
|
|
169
|
+
pid = spawn(*cmd)
|
|
170
|
+
Process.wait pid
|
|
171
|
+
if $?.exited?
|
|
172
|
+
if !$?.success?
|
|
173
|
+
exit $?.exitstatus
|
|
174
|
+
end
|
|
175
|
+
else
|
|
176
|
+
error "Command exited abnormally."
|
|
177
|
+
exit 1
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def run_inline_swallowing_interrupt(cmd)
|
|
183
|
+
begin
|
|
184
|
+
run_inline cmd
|
|
185
|
+
rescue Interrupt
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def run_or_fail(cmd, redact=nil)
|
|
190
|
+
put_command(cmd, redact)
|
|
191
|
+
Open3.popen3(*cmd) do |i, o, e, t|
|
|
192
|
+
i.close
|
|
193
|
+
if not t.value.success?
|
|
194
|
+
STDERR.write red_term_text(e.read)
|
|
195
|
+
exit t.value.exitstatus
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def run(cmd)
|
|
201
|
+
Open3.popen3(*cmd) do |i, o, e, t|
|
|
202
|
+
i.close
|
|
203
|
+
t.value
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def pipe(*cmds)
|
|
208
|
+
s = cmds.map { |x| x.join(" ") }
|
|
209
|
+
s = s.join(" | ")
|
|
210
|
+
STDERR.puts "+ #{s}"
|
|
211
|
+
Open3.pipeline(*cmds).each do |status|
|
|
212
|
+
unless status.success?
|
|
213
|
+
error "Piped command failed"
|
|
214
|
+
exit 1
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
data/lib/syncfiles.rb
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
require "open3"
|
|
2
|
+
|
|
3
|
+
module PmgmtLib
|
|
4
|
+
class SyncFiles
|
|
5
|
+
attr :c
|
|
6
|
+
|
|
7
|
+
def initialize(common)
|
|
8
|
+
@c = common
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def is_fswatch_installed()
|
|
12
|
+
status = c.run %W{which fswatch}
|
|
13
|
+
return status.success?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def src_vol_name()
|
|
17
|
+
env = c.load_env
|
|
18
|
+
"#{env.namespace}-src"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def output_vol_name()
|
|
22
|
+
env = c.load_env
|
|
23
|
+
"#{env.namespace}-out"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def get_src_volume_mount()
|
|
27
|
+
if is_fswatch_installed
|
|
28
|
+
%W{-v #{src_vol_name}:/w}
|
|
29
|
+
else
|
|
30
|
+
%W{-v #{ENV["PWD"]}:/w}
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def get_volume_mounts()
|
|
35
|
+
env = c.load_env
|
|
36
|
+
if env.static_file_dest
|
|
37
|
+
get_src_volume_mount + %W{-v #{output_vol_name}:/w/#{env.static_file_dest}}
|
|
38
|
+
else
|
|
39
|
+
get_src_volume_mount
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def log_file_name()
|
|
44
|
+
".rsync.log"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def log_message(s)
|
|
48
|
+
File.open(log_file_name, "a") do |file|
|
|
49
|
+
file.write s
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def get_dest_path(src, dst)
|
|
54
|
+
if dst.nil?
|
|
55
|
+
dst = src
|
|
56
|
+
end
|
|
57
|
+
dst = dst.split(/\//).reverse.drop(1).reverse.join("/")
|
|
58
|
+
if not dst.empty?
|
|
59
|
+
dst = "/w/#{dst}"
|
|
60
|
+
else
|
|
61
|
+
dst = "/w"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def start_rsync_container()
|
|
66
|
+
env = c.load_env
|
|
67
|
+
c.docker.requires_docker
|
|
68
|
+
c.docker.ensure_image("tjamet/rsync")
|
|
69
|
+
cmd = %W{
|
|
70
|
+
docker run -d
|
|
71
|
+
--name #{env.namespace}-rsync
|
|
72
|
+
-v #{src_vol_name}:/w
|
|
73
|
+
}
|
|
74
|
+
if env.static_file_dest
|
|
75
|
+
cmd += %W{-v #{output_vol_name}:/w/#{env.static_file_dest}}
|
|
76
|
+
end
|
|
77
|
+
c.run_inline cmd + %W{-e DAEMON=docker tjamet/rsync}
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def stop_rsync_container()
|
|
81
|
+
env = c.load_env
|
|
82
|
+
c.run_inline %W{docker rm -f #{env.namespace}-rsync}
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def rsync_path(src, dst, log)
|
|
86
|
+
env = c.load_env
|
|
87
|
+
dst = get_dest_path(src, dst)
|
|
88
|
+
rsync_remote_shell = "docker exec -i"
|
|
89
|
+
cmd = %W{
|
|
90
|
+
rsync --blocking-io -azlv --delete -e #{rsync_remote_shell}
|
|
91
|
+
#{src}
|
|
92
|
+
#{env.namespace}-rsync:#{dst}
|
|
93
|
+
}
|
|
94
|
+
if log
|
|
95
|
+
Open3.popen3(*cmd) do |i, o, e, t|
|
|
96
|
+
i.close
|
|
97
|
+
if not t.value.success?
|
|
98
|
+
c.error e.read
|
|
99
|
+
exit t.value.exitstatus
|
|
100
|
+
end
|
|
101
|
+
log_message o.read
|
|
102
|
+
end
|
|
103
|
+
else
|
|
104
|
+
c.run_inline cmd
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def link_static_file(src, dst)
|
|
109
|
+
cmd = %W{docker run --rm -w /w} + get_volume_mounts + %W{alpine ln -snf /w/#{src} /w/#{dst}}
|
|
110
|
+
c.run_inline cmd
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def link_static_files()
|
|
114
|
+
env = c.load_env
|
|
115
|
+
threads = []
|
|
116
|
+
foreach_static_file do |path, entry|
|
|
117
|
+
threads << Thread.new do
|
|
118
|
+
link_static_file "#{path}/#{entry}", "#{env.static_file_dest}/#{entry}"
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
threads.each do |t|
|
|
122
|
+
t.join
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def foreach_static_file()
|
|
127
|
+
env = c.load_env
|
|
128
|
+
Dir.foreach(env.static_file_src) do |entry|
|
|
129
|
+
unless [".", ".."].include?(entry)
|
|
130
|
+
yield env.static_file_src, entry
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def watch_path(src, dst)
|
|
136
|
+
Open3.popen3(*%W{fswatch -o #{src}}) do |stdin, stdout, stderr, thread|
|
|
137
|
+
Thread.current["pid"] = thread.pid
|
|
138
|
+
stdin.close
|
|
139
|
+
stdout.each_line do |_|
|
|
140
|
+
rsync_path src, dst, true
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def perform_initial_sync()
|
|
146
|
+
env = c.load_env
|
|
147
|
+
env.source_file_paths.each do |src_path|
|
|
148
|
+
# Copying using tar ensures the destination directories will be created.
|
|
149
|
+
c.pipe(
|
|
150
|
+
%W{env COPYFILE_DISABLE=1 tar -c #{src_path}},
|
|
151
|
+
%W{docker cp - #{env.namespace}-rsync:/w}
|
|
152
|
+
)
|
|
153
|
+
rsync_path src_path, nil, false
|
|
154
|
+
end
|
|
155
|
+
if env.static_file_src
|
|
156
|
+
c.pipe(
|
|
157
|
+
%W{env COPYFILE_DISABLE=1 tar -c #{env.static_file_src}},
|
|
158
|
+
%W{docker cp - #{env.namespace}-rsync:/w}
|
|
159
|
+
)
|
|
160
|
+
rsync_path env.static_file_src, nil, false
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def start_watching_sync()
|
|
165
|
+
env = c.load_env
|
|
166
|
+
File.open(log_file_name, "w") {} # Create and truncate if exists.
|
|
167
|
+
paths_to_watch = env.source_file_paths
|
|
168
|
+
if env.static_file_src
|
|
169
|
+
paths_to_watch += [env.static_file_src]
|
|
170
|
+
end
|
|
171
|
+
paths_to_watch.each do |src_path|
|
|
172
|
+
thread = Thread.new { watch_path src_path, nil }
|
|
173
|
+
at_exit {
|
|
174
|
+
Process.kill("HUP", thread["pid"])
|
|
175
|
+
thread.join
|
|
176
|
+
}
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def maybe_start_file_syncing()
|
|
181
|
+
system_name, _ = Open3.capture2("uname")
|
|
182
|
+
system_name.chomp!
|
|
183
|
+
env = c.load_env
|
|
184
|
+
if env.static_file_src
|
|
185
|
+
c.status "Linking static files..."
|
|
186
|
+
link_static_files
|
|
187
|
+
end
|
|
188
|
+
fswatch_installed = is_fswatch_installed
|
|
189
|
+
if system_name == "Darwin" and not fswatch_installed
|
|
190
|
+
c.error "fswatch is not installed."
|
|
191
|
+
STDERR.puts "File syncing will be extremely slow due to a performance problem in docker.\n" \
|
|
192
|
+
"Installing fswatch is highly recommended. Try:\n\n$ brew install fswatch\n\n"
|
|
193
|
+
end
|
|
194
|
+
if fswatch_installed
|
|
195
|
+
c.status "Starting rsync container..."
|
|
196
|
+
at_exit { stop_rsync_container }
|
|
197
|
+
start_rsync_container
|
|
198
|
+
c.status "Performing initial file sync..."
|
|
199
|
+
perform_initial_sync
|
|
200
|
+
start_watching_sync
|
|
201
|
+
c.status "Watching source files. See log at #{log_file_name}."
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: pmgmt
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- David Mohs
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2018-07-10 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: A library to make Ruby your preferred scripting language for dev scripts.
|
|
14
|
+
email: davidmohs@gmail.com
|
|
15
|
+
executables: []
|
|
16
|
+
extensions: []
|
|
17
|
+
extra_rdoc_files: []
|
|
18
|
+
files:
|
|
19
|
+
- lib/dockerhelper.rb
|
|
20
|
+
- lib/pmgmt.rb
|
|
21
|
+
- lib/syncfiles.rb
|
|
22
|
+
homepage: http://rubygems.org/gems/pmgmt
|
|
23
|
+
licenses:
|
|
24
|
+
- MIT
|
|
25
|
+
metadata: {}
|
|
26
|
+
post_install_message:
|
|
27
|
+
rdoc_options: []
|
|
28
|
+
require_paths:
|
|
29
|
+
- lib
|
|
30
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
31
|
+
requirements:
|
|
32
|
+
- - ">="
|
|
33
|
+
- !ruby/object:Gem::Version
|
|
34
|
+
version: '0'
|
|
35
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
40
|
+
requirements: []
|
|
41
|
+
rubyforge_project:
|
|
42
|
+
rubygems_version: 2.7.6
|
|
43
|
+
signing_key:
|
|
44
|
+
specification_version: 4
|
|
45
|
+
summary: Project management scripting library.
|
|
46
|
+
test_files: []
|