scout-camp 0.1.13 → 0.1.14

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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.vimproject +52 -11
  3. data/Rakefile +5 -0
  4. data/VERSION +1 -1
  5. data/bin/scout-camp +46 -0
  6. data/doc/terraform.md +188 -0
  7. data/lib/scout/aws/s3.rb +6 -4
  8. data/lib/scout/offsite/resource.rb +110 -5
  9. data/lib/scout/offsite/step.rb +21 -14
  10. data/lib/scout/offsite/sync.rb +38 -10
  11. data/lib/scout/offsite.rb +1 -0
  12. data/lib/scout/render/engine.rb +119 -0
  13. data/lib/scout/render/helpers.rb +92 -0
  14. data/lib/scout/render/resource.rb +54 -0
  15. data/lib/scout/render.rb +3 -0
  16. data/lib/scout/sinatra/auth.rb +158 -0
  17. data/lib/scout/sinatra/base/assets.rb +245 -0
  18. data/lib/scout/sinatra/base/favicon.rb +43 -0
  19. data/lib/scout/sinatra/base/headers.rb +77 -0
  20. data/lib/scout/sinatra/base/helpers.rb +14 -0
  21. data/lib/scout/sinatra/base/parameters.rb +147 -0
  22. data/lib/scout/sinatra/base/post_processing.rb +18 -0
  23. data/lib/scout/sinatra/base/session.rb +72 -0
  24. data/lib/scout/sinatra/base.rb +253 -0
  25. data/lib/scout/sinatra/entity.rb +259 -0
  26. data/lib/scout/sinatra/finder.rb +9 -0
  27. data/lib/scout/sinatra/fragment.rb +275 -0
  28. data/lib/scout/sinatra/htmx.rb +68 -0
  29. data/lib/scout/sinatra/knowledge_base.rb +14 -0
  30. data/lib/scout/sinatra/tool.rb +11 -0
  31. data/lib/scout/sinatra/workflow.rb +129 -0
  32. data/lib/scout-camp.rb +1 -1
  33. data/scout-camp.gemspec +39 -3
  34. data/scout_commands/find +83 -0
  35. data/scout_commands/glob +90 -0
  36. data/share/aws/lambda_function.rb +53 -30
  37. data/share/terraform/aws/efs_host/data.tf +1 -2
  38. data/share/terraform/aws/efs_host/main.tf +1 -1
  39. data/share/terraform/aws/efs_host/variables.tf +5 -1
  40. data/test/scout/render/test_engine.rb +88 -0
  41. data/test/scout/render/test_resource.rb +29 -0
  42. data/test/scout/sinatra/base/test_headers.rb +125 -0
  43. data/test/scout/sinatra/base/test_parameters.rb +88 -0
  44. data/test/scout/sinatra/test_base.rb +27 -0
  45. data/test/scout/sinatra/test_entity.rb +44 -0
  46. data/test/scout/sinatra/test_render.rb +44 -0
  47. data/test/scout/sinatra/test_workflow.rb +157 -0
  48. data/test/test_helper.rb +26 -0
  49. metadata +103 -2
@@ -0,0 +1,245 @@
1
+ module SinatraScoutAssets
2
+ def self.registered(app)
3
+ app.helpers do
4
+ def reset_js_css
5
+ @recorded_js_files = []
6
+ @recorded_css_files = []
7
+ end
8
+
9
+ def recorded_js_files_global
10
+ @recorded_js_files ||= []
11
+ end
12
+
13
+ def recorded_css_files_global
14
+ @recorded_css_files ||= []
15
+ end
16
+
17
+ def recorded_js_files
18
+ global = recorded_js_files_global
19
+ return global unless @step
20
+ (global + (@step.info[:js_files] || [])).uniq
21
+ end
22
+
23
+ def recorded_css_files
24
+ global = recorded_css_files_global
25
+ return global unless @step
26
+ (global + (@step.info[:css_files] || [])).uniq
27
+ end
28
+
29
+ def record_js_global(file)
30
+ if Array === file
31
+ recorded_js_files.concat file
32
+ else
33
+ recorded_js_files << file
34
+ end
35
+ end
36
+
37
+ def record_css_global(file)
38
+ if Array === file
39
+ recorded_css_files.concat file
40
+ else
41
+ recorded_css_files << file
42
+ end
43
+ end
44
+
45
+ def record_js(js_files)
46
+ return record_js_global(js_files) unless @step
47
+ js_files = [js_files] unless Array === js_files
48
+ current_js_files = @step.info[:js_files] || []
49
+
50
+ new_files = js_files - current_js_files
51
+ @step.set_info :js_files, new_files if new_files.any?
52
+ end
53
+
54
+ def record_css(css_files)
55
+ return record_css_global(css_files) unless @step
56
+ css_files = [css_files] unless Array === css_files
57
+ current_css_files = @step.info[:css_files] || []
58
+
59
+ new_files = css_files - current_css_files
60
+ @step.set_info :css_files, new_files if new_files.any?
61
+ end
62
+
63
+ def link_css(file)
64
+ file << "?_update=reload" if @debug_css
65
+ html_tag('link', nil, :rel => 'stylesheet', :type => 'text/css', :href => file)
66
+ end
67
+
68
+ def link_js(file)
69
+ html_tag('script', " ", :src => file, :type => 'text/javascript')
70
+ end
71
+
72
+ def purge_paths(paths)
73
+ @served_paths ||= []
74
+ new = paths - @served_paths
75
+ @served_paths.concat new
76
+ new
77
+ end
78
+
79
+ def serve_css
80
+ md5 = Misc.digest(recorded_css_files * ",")
81
+ filename = ScoutRender.cache_dir["all_css-#{md5}.css"].find
82
+
83
+ paths = recorded_css_files.collect do |file|
84
+ if Open.remote?(file)
85
+ path = file
86
+ else
87
+ path = ScoutRender.find_js(file)
88
+ path = ScoutRender.find_js("public/#{file}") unless path.exists?
89
+ end
90
+ path
91
+ end.uniq
92
+
93
+ paths = purge_paths paths
94
+
95
+ update = _update == :css
96
+ checks = paths.select{|p| Open.mtime(p) }
97
+
98
+ Persist.persist 'all', :text, prefix: 'css', path: filename, other: {files: paths}, check: checks, update: update, no_load: true do
99
+ Log.debug{ "Regenerating CSS Compressed file: #{ filename }" }
100
+ paths.collect do |path|
101
+ TmpFile.with_file do |tmpfile|
102
+ cmd_str = "-i '#{path}' -o '#{tmpfile}'"
103
+ CMD.cmd(:tailwindcss, cmd_str)
104
+ Open.read tmpfile
105
+ end
106
+ end * "\n"
107
+ end
108
+
109
+ res = "<link href='/file/#{File.basename(filename)}' rel='stylesheet' type='text/css'/>"
110
+
111
+ res
112
+ end
113
+
114
+ def serve_js
115
+ md5 = Misc.digest(recorded_js_files * ",")
116
+ filename = ScoutRender.cache_dir["all_js-#{md5}.js"].find
117
+
118
+ paths = recorded_js_files.collect do |file|
119
+ if Open.remote?(file)
120
+ path = file
121
+ else
122
+ path = ScoutRender.find_js(file)
123
+ path = ScoutRender.find_js("public/#{file}") unless path.exists?
124
+ end
125
+ path
126
+ end.uniq
127
+
128
+ paths = purge_paths paths
129
+
130
+ update = _update == :js
131
+
132
+ minify = false
133
+ checks = paths.select{|p| Open.mtime(p) }
134
+
135
+ Persist.persist 'all', :text, prefix: 'js', path: filename, other: {files: paths}, check: checks, update: update, no_load: true do
136
+ Log.debug{ "Regenerating JS Compressed file: #{ filename }, from: #{Log.fingerprint paths}" }
137
+ TmpFile.with_file nil, false do |tmp_file|
138
+ Open.write tmp_file do |f|
139
+ paths.each do |path|
140
+ f.write(Open.read(path) + "\n")
141
+ end
142
+ end
143
+
144
+ cmd_str = "esbuild ".dup
145
+ cmd_str << "'#{tmp_file}' "
146
+ cmd_str << "--outfile='#{filename}' "
147
+ cmd_str << "--bundle " if paths.length > 1
148
+ cmd_str << "--minify " if minify
149
+ cmd_str << "--platform='browser' "
150
+ CMD.cmd(:npx, cmd_str)
151
+ end
152
+ end
153
+
154
+ res = "<script src='/file/#{File.basename(filename)}' type='text/javascript' defer></script>"
155
+
156
+ recorded_js_files.clear
157
+
158
+ res
159
+ end
160
+
161
+ def mime_file(file, content_type = nil)
162
+ if file.end_with?('.js')
163
+ content_type = 'text/javascript'
164
+ elsif file.end_with?('.css')
165
+ content_type = 'text/css'
166
+ else
167
+ content_type = 'text/html'
168
+ end if content_type.nil?
169
+
170
+ Log.debug "Serving #{file} as #{content_type}"
171
+ content_type content_type if content_type
172
+ send_file(file)
173
+ end
174
+ end
175
+
176
+ # helpers for REST-style responses & params
177
+ # expose method to register a workflow at runtime
178
+ app.get "/plugins/*" do
179
+ splat = consume_parameter(:splat)
180
+
181
+ splat.unshift 'public/plugins'
182
+
183
+ name = splat * "/"
184
+
185
+ file = ScoutRender.find_resource(name)
186
+ status 200
187
+ mime_file file
188
+ end
189
+
190
+ app.get '/(stylesheets|css)/*' do
191
+ splat = consume_parameter(:splat)
192
+
193
+ splat.unshift 'public/css'
194
+
195
+ name = splat * "/"
196
+
197
+ file = ScoutRender.find_resource(name)
198
+
199
+ if file.exists?
200
+ content_type 'text/css', :charset => 'utf-8'
201
+ cache_control :public, :max_age => 360000 if production?
202
+
203
+ mime_file file, false
204
+ else
205
+ splat.shift
206
+
207
+ splat.unshift 'compass'
208
+
209
+ name = splat * "/"
210
+
211
+ content_type 'text/css', :charset => 'utf-8'
212
+ cache_control :public, :max_age => 360000 if production?
213
+
214
+ render_template(name.sub(/\.css/, '.sass'))
215
+ end
216
+ end
217
+
218
+ app.get '/js/*' do
219
+ splat = consume_parameter(:splat)
220
+
221
+ splat.unshift 'public/js'
222
+
223
+ name = splat * "/"
224
+
225
+ file = ScoutRender.find_resource(name)
226
+ content_type 'application/javascript'
227
+ status 200
228
+ mime_file file, false
229
+ end
230
+
231
+ app.get '/file/*' do
232
+ splat = consume_parameter(:splat)
233
+
234
+ name = splat * "/"
235
+ if ScoutRender.cache_dir[name].exists?
236
+ mime_file ScoutRender.cache_dir[name].find
237
+ end
238
+
239
+ splat.unshift 'public'
240
+ name = splat * "/"
241
+ file = ScoutRender.find_resource(name)
242
+ mime_file file
243
+ end
244
+ end
245
+ end
@@ -0,0 +1,43 @@
1
+ module SinatraScoutFavicon
2
+ def self.registered(app)
3
+ app.get '/favicon.ico' do
4
+ content_type 'image/svg+xml'
5
+ cache_control :public, max_age: 86_400 # 1 day
6
+
7
+ <<~SVG
8
+ <?xml version="1.0" encoding="UTF-8"?>
9
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" role="img" aria-label="Compass favicon">
10
+ <!-- outer circle -->
11
+ <circle cx="32" cy="32" r="28" fill="none" stroke="currentColor" stroke-width="2" />
12
+ <!-- ticks for N/E/S/W -->
13
+ <line x1="32" y1="4" x2="32" y2="10" fill="none" stroke="currentColor" stroke-width="2" />
14
+ <line x1="60" y1="32" x2="54" y2="32" fill="none" stroke="currentColor" stroke-width="2" />
15
+ <line x1="32" y1="60" x2="32" y2="54" fill="none" stroke="currentColor" stroke-width="2" />
16
+ <line x1="4" y1="32" x2="10" y2="32" fill="none" stroke="currentColor" stroke-width="2" />
17
+ <!-- inner degree markers -->
18
+ <g stroke="currentColor" stroke-width="1">
19
+ <line x1="32" y1="12" x2="32" y2="14"/>
20
+ <line x1="45" y1="19" x2="43" y2="21"/>
21
+ <line x1="52" y1="32" x2="50" y2="32"/>
22
+ <line x1="45" y1="45" x2="43" y2="43"/>
23
+ <line x1="32" y1="52" x2="32" y2="50"/>
24
+ <line x1="19" y1="45" x2="21" y2="43"/>
25
+ <line x1="12" y1="32" x2="14" y2="32"/>
26
+ <line x1="19" y1="19" x2="21" y2="21"/>
27
+ </g>
28
+ <!-- compass needle (line drawing) -->
29
+ <g stroke="currentColor" stroke-width="1.8" stroke-linejoin="round" stroke-linecap="round" fill="none">
30
+ <!-- main spine -->
31
+ <line x1="32" y1="32" x2="42" y2="12"/>
32
+ <line x1="32" y1="32" x2="22" y2="52"/>
33
+ <!-- needle outlines (simple triangular tips) -->
34
+ <path d="M42 12 L36 20 L32 18 Z" fill="none" stroke="currentColor" stroke-width="1.6"/>
35
+ <path d="M22 52 L28 44 L32 46 Z" fill="none" stroke="currentColor" stroke-width="1.6"/>
36
+ </g>
37
+ <!-- center hub -->
38
+ <circle cx="32" cy="32" r="2" fill="currentColor" />
39
+ </svg>
40
+ SVG
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,77 @@
1
+ module SinatraScoutHeaders
2
+ def self.registered(app)
3
+ app.helpers do
4
+ def environment
5
+ settings.environment
6
+ end
7
+
8
+ def production?
9
+ environment == :production
10
+ end
11
+
12
+ def development?
13
+ environment == :development
14
+ end
15
+
16
+ def script_name
17
+ request.script_name ||= request.env["HTTP_SCRIPT_NAME"]
18
+ end
19
+
20
+ def xhr?
21
+ request.xhr?
22
+ end
23
+
24
+ def ajax?
25
+ request.xhr?
26
+ end
27
+
28
+ def request_method
29
+ request.env["REQUEST_METHOD"]
30
+ end
31
+
32
+ def post?
33
+ request_method.to_s.downcase == 'post'
34
+ end
35
+
36
+ def clean_uri(uri)
37
+ return nil if uri.nil?
38
+ remove_GET_param(uri, ["_update", "_"])
39
+ end
40
+
41
+ def original_uri
42
+ clean_uri(request.env["REQUEST_URI"])
43
+ end
44
+
45
+ def post_uri
46
+ new_params = {}
47
+ params.each do |k,v|
48
+ if m = k.match(/(.*)__param_file/)
49
+ new_params[m[1]] = v['filename']
50
+ else
51
+ new_params[k] = v
52
+ end
53
+ end
54
+ hash = Misc.digest(new_params)
55
+ params[:_layout]
56
+ end
57
+
58
+ def path_info
59
+ @path_info ||= request.env["PATH_INFO"]
60
+ end
61
+
62
+ def query
63
+ @query ||= request.env["QUERY_STRING"]
64
+ end
65
+
66
+ def fullpath
67
+ @fullpath ||= (query && ! query.empty?) ? clean_uri(path_info + "?" + query) : path_info
68
+ end
69
+
70
+ alias url fullpath
71
+
72
+ def script_name
73
+ @script_name ||= request.script_name = request.env["HTTP_SCRIPT_NAME"]
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,14 @@
1
+ require_relative 'post_processing'
2
+ module SinatraScoutHelpers
3
+ def self.registered(app)
4
+
5
+ app.helpers do
6
+ def format_name(name)
7
+ parts = name.split("_")
8
+ hash = parts.pop
9
+ clean_name = parts * "_"
10
+ "<span class='name' jobname='#{ name }'>#{ clean_name }</span> <span class='hash'>#{ hash }</span>"
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,147 @@
1
+ module SinatraScoutParameters
2
+ def self.registered(app)
3
+ app.set :common_parameters, []
4
+
5
+ app.define_singleton_method(:register_common_parameter) do |name,type=:string,default=nil,&block|
6
+ settings.common_parameters << name
7
+ app.helpers do
8
+ attr_accessor name
9
+
10
+ define_method(name) do
11
+ value = self.instance_variable_get(:"@#{name}")
12
+ return value unless value.nil?
13
+ value = consume_parameter(name)
14
+ if value.nil?
15
+ value = if default
16
+ default
17
+ elsif block
18
+ self.instance_eval &block
19
+ else
20
+ nil
21
+ end
22
+ end
23
+
24
+ value = case type
25
+ when :string
26
+ value.to_s if value
27
+ when :symbol
28
+ if String === value
29
+ value.to_sym
30
+ else
31
+ value
32
+ end
33
+ when :integer
34
+ value.to_i
35
+ when :float
36
+ value.to_f
37
+ when :boolean
38
+ value.to_s.downcase == "true" unless value.nil?
39
+ when :escaped
40
+ restore_element(value) if value
41
+ when :array
42
+ value = case value
43
+ when Array
44
+ value
45
+ when String
46
+ value.split(",")
47
+ else
48
+ value
49
+ end
50
+ end
51
+
52
+ self.instance_variable_set(:"@#{name}", value)
53
+ end
54
+ end
55
+ end
56
+
57
+ app.helpers do
58
+ def clean_element(elem)
59
+ elem.to_s.gsub('&', '--AND--').
60
+ gsub('/', '-..-').
61
+ gsub("|", '-...-').
62
+ gsub('%', 'o-o').
63
+ gsub('[','(.-(').
64
+ gsub(']',').-)')
65
+ end
66
+
67
+ def restore_element(elem)
68
+ CGI.unescape(CGI.unescape(
69
+ elem.gsub('--AND--', '&').
70
+ gsub('-..-', '/').
71
+ gsub('-...-', '|').
72
+ gsub('o-o', '%').
73
+ gsub('(.-(','[').
74
+ gsub(').-)',']')
75
+ ))
76
+ end
77
+
78
+ def consume_parameter(name, source = params)
79
+ return nil if source.nil?
80
+ val = IndiferentHash.process_options source, name
81
+ val = nil if val == ''
82
+ val
83
+ end
84
+
85
+ def clean_params
86
+ @clean_params ||= begin
87
+ params = IndiferentHash.setup(self.params)
88
+ params.keys.each do |param|
89
+ if param =~ /(.*)_checkbox_false$/
90
+ params[$1] = false if params[$1].nil?
91
+ params.delete param
92
+ elsif param =~ /(.*)_upload_file$/
93
+ file = params[param]
94
+ params[$1] = file['tempfile'].read
95
+ params[$1 + "_upload_filename"] = file['filename']
96
+ elsif param.to_s.start_with? '_'
97
+ params.delete param
98
+ end
99
+ end
100
+ params
101
+ end
102
+ end
103
+
104
+ def process_common_parameters
105
+ settings.common_parameters.each{|name,*_| self.send(name) }
106
+ end
107
+
108
+ def form(**kwgars, &block)
109
+ inputs = []
110
+ input_types = {}
111
+ input_descriptions = {}
112
+ input_defaults = {}
113
+ input_options = {}
114
+
115
+ self.define_singleton_method :input do |name, type=nil, description=nil, default=nil, options=nil|
116
+ inputs << name
117
+ input_types[name] = type
118
+ input_descriptions[name] = description
119
+ input_defaults[name] = default
120
+ input_options[name] = options
121
+ end
122
+ self.instance_exec &block
123
+
124
+ self.singleton_class.remove_method(:input)
125
+
126
+
127
+ render_partial 'partial/form', kwgars.merge( inputs: inputs, types: input_types, descriptions: input_descriptions, defaults: input_defaults, options: input_options)
128
+ end
129
+ end
130
+
131
+ app.register_common_parameter(:splat, :array)
132
+ app.register_common_parameter(:_layout, :boolean) do ! ajax? end
133
+ app.register_common_parameter(:_format, :symbol) do :html end
134
+ app.register_common_parameter(:_update, :symbol) do
135
+ if development? && ! _step
136
+ :development
137
+ elsif request_method != 'GET'
138
+ request_method
139
+ end
140
+ end
141
+ app.register_common_parameter(:_cache_type, :symbol, :asynchronous)
142
+ app.register_common_parameter(:_debug_js, :boolean)
143
+ app.register_common_parameter(:_debug_css, :boolean)
144
+ app.register_common_parameter(:_step, :string)
145
+ app.register_common_parameter(:_)
146
+ end
147
+ end
@@ -0,0 +1,18 @@
1
+ module SinatraScoutPostProcessing
2
+ def self.registered(app)
3
+ app.set :post_processing_blocks, []
4
+
5
+ app.define_singleton_method(:register_post_processing) do |&block|
6
+ settings.post_processing_blocks << block
7
+ end
8
+
9
+ app.helpers do
10
+ def post_processing(step)
11
+ return unless settings.post_processing_blocks
12
+ settings.post_processing_blocks.each do |block|
13
+ self.instance_exec step, &block
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,72 @@
1
+ require "sinatra"
2
+
3
+ module SinatraScoutSession
4
+ def self.registered(app)
5
+ secret = Scout::Config.get :secret, :sinatra_session, :sinatra, :session, env:"SESSION_SECRET", default: "scout_and_rbbt_super_secret"
6
+
7
+ app.configure do
8
+ app.enable :sessions
9
+ app.set :sessions, true
10
+ app.set :session_secret, secret
11
+ end
12
+
13
+ app.use Rack::Session::Cookie, secret: secret
14
+
15
+ app.get '/debug_session' do
16
+ "Session contents: #{session.inspect}"
17
+ end
18
+
19
+ app.post_get '/preference/:key' do
20
+ key = consume_parameter :key
21
+ value = consume_parameter :value
22
+
23
+ if value.nil?
24
+ consume_parameter :key
25
+ case _format
26
+ when :json
27
+ content_type 'application/json'
28
+ get_preference(key).to_json
29
+ else
30
+ get_preference(key)
31
+ end
32
+ else
33
+
34
+ case _format
35
+ when :json
36
+ value = JSON.parse(value)
37
+ else
38
+ record_preference(key, value)
39
+ end
40
+ halt 200
41
+ end
42
+ end
43
+
44
+ app.helpers do
45
+
46
+ def csrf_token
47
+ session[:csrf] ||= SecureRandom.hex(16)
48
+ end
49
+
50
+ def preferences
51
+ session['preferences'] ||= {}
52
+ end
53
+
54
+ def record_preference(key, value)
55
+ preferences[key] = value
56
+ end
57
+
58
+ def get_preference(key)
59
+ preferences[key]
60
+ end
61
+
62
+ def preference_url(key, value=nil, params = {})
63
+ url = "/preference/#{key}"
64
+ if value
65
+ add_GET_params("/preference/#{key}", params.merge(value: value))
66
+ else
67
+ add_GET_params("/preference/#{key}", params)
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end