command_proposal 1.0.13 → 1.0.16
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/assets/javascripts/command_proposal/console.js +73 -35
- data/app/assets/stylesheets/command_proposal/tables.scss +3 -0
- data/app/controllers/command_proposal/iterations_controller.rb +20 -10
- data/app/controllers/command_proposal/tasks_controller.rb +3 -3
- data/app/helpers/command_proposal/permissions_helper.rb +9 -4
- data/app/jobs/command_proposal/command_runner_job.rb +18 -2
- data/app/models/command_proposal/iteration.rb +4 -1
- data/app/models/command_proposal/service/proposal_presenter.rb +1 -0
- data/app/views/command_proposal/tasks/_console_lines.html.erb +3 -3
- data/app/views/command_proposal/tasks/_lines.html.erb +1 -1
- data/app/views/command_proposal/tasks/_past_iterations_list.html.erb +14 -2
- data/lib/command_proposal/configuration.rb +2 -0
- data/lib/command_proposal/services/runner.rb +37 -16
- data/lib/command_proposal/services/shut_down.rb +1 -1
- data/lib/command_proposal/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 43e714b8d84f486d67fabcf4d171cc38f6fe3e97c1e4cd776b558ef9feef2bb0
|
4
|
+
data.tar.gz: 5107629d432ed32fb26bd3fbfe56094287b1a9a0eb2feaf72b1e9f493b3d1dfd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 283e95ed280a541f6e607e2802068664244d532186448cafe42850763bb318159335b33b64e5411c729a65cbd427de94b6e672154efe0231487a71ba8023e659
|
7
|
+
data.tar.gz: bbece28324234cfee33481dbdeef5fdadd0b90ff982bb3eeabeaa76e6732c558298ed8dd7a0e313f9304b574dc7559e13187c4bdc3e7cec0a3ed7c0c85102e9c
|
@@ -142,14 +142,16 @@ cmdDocReady(function() {
|
|
142
142
|
|
143
143
|
console_input.textContent = ""
|
144
144
|
|
145
|
-
|
146
|
-
|
145
|
+
if (!(/^[\s\n]*$/.test(line.textContent))) {
|
146
|
+
var result = document.createElement("div")
|
147
|
+
result.classList.add("result")
|
147
148
|
|
148
|
-
|
149
|
-
|
150
|
-
|
149
|
+
var spinner = document.createElement("i")
|
150
|
+
spinner.className = "fa fa-circle-o-notch fa-spin cmd-icon-grey"
|
151
|
+
result.append(spinner)
|
151
152
|
|
152
|
-
|
153
|
+
line.appendChild(result)
|
154
|
+
}
|
153
155
|
|
154
156
|
lines.appendChild(line)
|
155
157
|
stored_entry = undefined
|
@@ -176,41 +178,77 @@ cmdDocReady(function() {
|
|
176
178
|
body: JSON.stringify(params),
|
177
179
|
done: function(res, status, req) {
|
178
180
|
if (status == 200) {
|
179
|
-
|
180
|
-
line.querySelector(".result").remove()
|
181
|
-
|
182
|
-
var result = document.createElement("div")
|
183
|
-
result.classList.add("result")
|
184
|
-
|
185
|
-
if (json.error) {
|
186
|
-
result.classList.add("cmd-error")
|
187
|
-
result.textContent = json.error
|
188
|
-
} else {
|
189
|
-
var truncate = 2000
|
190
|
-
if (json.result.length > truncate-3) {
|
191
|
-
result.textContent = json.result.slice(0, truncate-3) + "..."
|
192
|
-
var encoded = encodeURIComponent(json.result)
|
193
|
-
|
194
|
-
var download = document.createElement("a")
|
195
|
-
download.classList.add("cmd-truncated-download")
|
196
|
-
download.setAttribute("href", "data:application/txt," + encoded)
|
197
|
-
download.setAttribute("download", "result.txt")
|
198
|
-
download.textContent = "Output truncated. Click here to download full result."
|
199
|
-
|
200
|
-
line.insertAdjacentElement("afterend", download)
|
201
|
-
} else {
|
202
|
-
result.textContent = json.result
|
203
|
-
}
|
204
|
-
}
|
205
|
-
|
206
|
-
line.appendChild(result)
|
181
|
+
handleSuccessfulCommand(evt, line, JSON.parse(res))
|
207
182
|
} else {
|
208
183
|
console.log("Error: ", res, req);
|
209
184
|
}
|
210
|
-
evt.finish()
|
211
185
|
}
|
212
186
|
})
|
213
187
|
})
|
214
188
|
}
|
189
|
+
|
190
|
+
function handleSuccessfulCommand(evt, line, json) {
|
191
|
+
if (json.error) {
|
192
|
+
addLineResult(line, json.error, "cmd-error")
|
193
|
+
} else if (json.status != "started") {
|
194
|
+
addLineResult(line, json.result, json.status == "failed" ? "cmd-error" : "")
|
195
|
+
} else {
|
196
|
+
return setTimeout(function() { pollIteration(evt, line, json.results_endpoint) }, 2000)
|
197
|
+
}
|
198
|
+
|
199
|
+
evt.finish()
|
200
|
+
}
|
201
|
+
|
202
|
+
function addLineResult(line, text, result_class) {
|
203
|
+
line.querySelector(".result").remove()
|
204
|
+
|
205
|
+
if (!text || /^[\s\n]*$/.test(text)) { return }
|
206
|
+
|
207
|
+
var result = document.createElement("div")
|
208
|
+
result.classList.add("result")
|
209
|
+
if (result_class) { result.classList.add(result_class) }
|
210
|
+
|
211
|
+
var truncate = 2000
|
212
|
+
if (text.length > truncate-3) {
|
213
|
+
result.textContent = text.slice(0, truncate-3) + "..."
|
214
|
+
var encoded = encodeURIComponent(text)
|
215
|
+
|
216
|
+
var download = document.createElement("a")
|
217
|
+
download.classList.add("cmd-truncated-download")
|
218
|
+
download.setAttribute("href", "data:application/txt," + encoded)
|
219
|
+
download.setAttribute("download", "result.txt")
|
220
|
+
download.textContent = "Output truncated. Click here to download full result."
|
221
|
+
|
222
|
+
line.insertAdjacentElement("afterend", download)
|
223
|
+
} else {
|
224
|
+
result.textContent = text
|
225
|
+
}
|
226
|
+
|
227
|
+
line.appendChild(result)
|
228
|
+
}
|
229
|
+
|
230
|
+
function pollIteration(evt, line, endpoint) {
|
231
|
+
var client = new HttpClient()
|
232
|
+
client.get(endpoint, {
|
233
|
+
headers: {
|
234
|
+
"Content-Type": "application/json",
|
235
|
+
"X-CSRF-Token": $.rails.csrfToken()
|
236
|
+
},
|
237
|
+
done: function(res, status, req) {
|
238
|
+
if (status == 200) {
|
239
|
+
var json = JSON.parse(res)
|
240
|
+
if (json.status == "started") {
|
241
|
+
setTimeout(function() { pollIteration(evt, line, endpoint) }, 2000)
|
242
|
+
} else {
|
243
|
+
addLineResult(line, json.result, json.status == "failed" ? "cmd-error" : "")
|
244
|
+
evt.finish()
|
245
|
+
}
|
246
|
+
} else {
|
247
|
+
console.log("Error: ", res, req);
|
248
|
+
evt.finish()
|
249
|
+
}
|
250
|
+
}
|
251
|
+
})
|
252
|
+
}
|
215
253
|
}
|
216
254
|
})
|
@@ -9,6 +9,16 @@ class ::CommandProposal::IterationsController < ::CommandProposal::EngineControl
|
|
9
9
|
|
10
10
|
layout "application"
|
11
11
|
|
12
|
+
def show
|
13
|
+
@iteration = ::CommandProposal::Iteration.find(params[:id])
|
14
|
+
|
15
|
+
render json: {
|
16
|
+
results_endpoint: cmd_path(@iteration),
|
17
|
+
result: @iteration.result,
|
18
|
+
status: @iteration.status
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
12
22
|
def create
|
13
23
|
return error!("You do not have permission to run commands.") unless can_command?
|
14
24
|
|
@@ -17,32 +27,32 @@ class ::CommandProposal::IterationsController < ::CommandProposal::EngineControl
|
|
17
27
|
return error!("Can only run commands on type: :console") unless @task.console?
|
18
28
|
return error!("Session has not been approved.") unless has_approval?(@task)
|
19
29
|
|
20
|
-
if @task.iterations.
|
21
|
-
runner = ::CommandProposal.sessions["task:#{@task.id}"]
|
22
|
-
elsif @task.iterations.one?
|
30
|
+
if @task.iterations.one?
|
23
31
|
# Track console details in first iteration
|
24
32
|
@task.first_iteration.update(started_at: Time.current, status: :started)
|
25
|
-
runner = ::CommandProposal::Services::Runner.new
|
26
|
-
::CommandProposal.sessions["task:#{@task.id}"] = runner
|
27
33
|
end
|
28
34
|
|
29
|
-
return error!("Session has expired. Please start a new session.") if runner.nil?
|
30
|
-
|
31
35
|
@task.user = command_user # Separate from update to ensure it's set first
|
32
36
|
@task.update(code: params[:code]) # Creates a new iteration
|
33
37
|
@iteration = @task.current_iteration
|
34
38
|
@iteration.update(status: :approved) # Task was already approved, and this is line-by-line
|
35
39
|
|
36
40
|
# async, but wait for the job to finish
|
37
|
-
::CommandProposal::CommandRunnerJob.perform_later(@iteration.id)
|
41
|
+
::CommandProposal::CommandRunnerJob.perform_later(@iteration.id, "task:#{@task.id}")
|
42
|
+
|
43
|
+
max_wait_seconds = 3
|
38
44
|
loop do
|
39
|
-
|
45
|
+
break unless max_wait_seconds.positive?
|
46
|
+
|
47
|
+
max_wait_seconds -= sleep 0.2
|
40
48
|
|
41
49
|
break if @iteration.reload.complete?
|
42
50
|
end
|
43
51
|
|
44
52
|
render json: {
|
45
|
-
|
53
|
+
results_endpoint: cmd_path(@iteration),
|
54
|
+
result: @iteration.result,
|
55
|
+
status: @iteration.status
|
46
56
|
}
|
47
57
|
end
|
48
58
|
|
@@ -53,12 +53,12 @@ class ::CommandProposal::TasksController < ::CommandProposal::EngineController
|
|
53
53
|
def create
|
54
54
|
@task = ::CommandProposal::Task.new(task_params.except(:code))
|
55
55
|
@task.user = command_user
|
56
|
-
@task.skip_approval = true unless approval_required?
|
56
|
+
@task.skip_approval = true unless approval_required?(@task.session_type)
|
57
57
|
|
58
58
|
# Cannot create the iteration until the task is created, so save then update
|
59
59
|
if @task.save && @task.update(task_params)
|
60
60
|
if @task.console?
|
61
|
-
@task.
|
61
|
+
@task.code = nil # Creates a blank iteration to track approval
|
62
62
|
redirect_to cmd_path(@task)
|
63
63
|
else
|
64
64
|
redirect_to cmd_path(:edit, @task)
|
@@ -72,7 +72,7 @@ class ::CommandProposal::TasksController < ::CommandProposal::EngineController
|
|
72
72
|
def update
|
73
73
|
@task = ::CommandProposal::Task.find_by!(friendly_id: params[:id])
|
74
74
|
@task.user = command_user
|
75
|
-
@task.skip_approval = true unless approval_required?
|
75
|
+
@task.skip_approval = true unless approval_required?(@task.session_type)
|
76
76
|
|
77
77
|
if @task.update(task_params)
|
78
78
|
redirect_to cmd_path(@task)
|
@@ -9,7 +9,7 @@ module CommandProposal
|
|
9
9
|
|
10
10
|
def can_approve?(iteration)
|
11
11
|
return false unless permitted_to_use?
|
12
|
-
return true unless approval_required?
|
12
|
+
return true unless approval_required?(iteration.task.session_type)
|
13
13
|
return if iteration.nil?
|
14
14
|
|
15
15
|
command_user.try("#{cmd_config.role_scope}?") && !current_is_author?(iteration)
|
@@ -17,13 +17,18 @@ module CommandProposal
|
|
17
17
|
|
18
18
|
def has_approval?(task)
|
19
19
|
return false unless permitted_to_use?
|
20
|
-
return true unless approval_required?
|
20
|
+
return true unless approval_required?(task.session_type)
|
21
21
|
|
22
22
|
task&.approved?
|
23
23
|
end
|
24
24
|
|
25
|
-
def approval_required?
|
26
|
-
cmd_config.approval_required?
|
25
|
+
def approval_required?(task_type=nil)
|
26
|
+
return false unless cmd_config.approval_required?
|
27
|
+
return true if task_type.blank?
|
28
|
+
|
29
|
+
skips = cmd_config.skip_approval_for_types.presence || []
|
30
|
+
|
31
|
+
Array.wrap(skips).map(&:to_sym).exclude?(task_type.to_sym)
|
27
32
|
end
|
28
33
|
|
29
34
|
def current_is_author?(iteration)
|
@@ -2,10 +2,26 @@ module CommandProposal
|
|
2
2
|
class CommandRunnerJob < ApplicationJob
|
3
3
|
queue_as :default
|
4
4
|
|
5
|
-
def perform(iteration_id)
|
5
|
+
def perform(iteration_id, runner_key=nil)
|
6
6
|
iteration = ::CommandProposal::Iteration.find(iteration_id)
|
7
|
+
runner = ::CommandProposal.sessions[runner_key] if runner_key.present?
|
7
8
|
|
8
|
-
|
9
|
+
if runner_key.present? && runner.blank?
|
10
|
+
if iteration.task.console? && iteration.task.iterations.count > 2 # 1 for init, and the 1 for current running code
|
11
|
+
return ::CommandProposal::Services::Runner.new.quick_fail(
|
12
|
+
iteration,
|
13
|
+
"Session has expired. Please start a new session."
|
14
|
+
)
|
15
|
+
else
|
16
|
+
runner = ::CommandProposal::Services::Runner.new
|
17
|
+
end
|
18
|
+
|
19
|
+
::CommandProposal.sessions[runner_key] = runner if runner_key.present?
|
20
|
+
else
|
21
|
+
runner ||= ::CommandProposal::Services::Runner.new
|
22
|
+
end
|
23
|
+
|
24
|
+
runner.execute(iteration)
|
9
25
|
end
|
10
26
|
end
|
11
27
|
end
|
@@ -23,7 +23,6 @@ class ::CommandProposal::Iteration < ApplicationRecord
|
|
23
23
|
|
24
24
|
TRUNCATE_COUNT = 2000
|
25
25
|
# Also hardcoded in JS: app/assets/javascripts/command_proposal/console.js
|
26
|
-
PAGINATION_PER = 2
|
27
26
|
|
28
27
|
has_many :comments
|
29
28
|
belongs_to :task
|
@@ -66,6 +65,10 @@ class ::CommandProposal::Iteration < ApplicationRecord
|
|
66
65
|
task.primary_iteration == self
|
67
66
|
end
|
68
67
|
|
68
|
+
def approved?
|
69
|
+
super || (session_type == "function" && approved_at?)
|
70
|
+
end
|
71
|
+
|
69
72
|
def complete?
|
70
73
|
success? || failed? || cancelled? || terminated?
|
71
74
|
end
|
@@ -13,6 +13,7 @@ module CommandProposal
|
|
13
13
|
delegate :description, to: :iteration
|
14
14
|
delegate :args, to: :iteration
|
15
15
|
delegate :code, to: :iteration
|
16
|
+
delegate :result, to: :iteration
|
16
17
|
delegate :status, to: :iteration
|
17
18
|
delegate :approved_at, to: :iteration
|
18
19
|
delegate :started_at, to: :iteration
|
@@ -1,18 +1,18 @@
|
|
1
1
|
<% if lines.none? && !(skip_empty ||= false) -%><div class="line"></div><% end
|
2
2
|
-%><% lines.each do |iteration| -%>
|
3
3
|
<div class="line"><%= iteration.code
|
4
|
-
-%><div class="result"><%=
|
4
|
+
-%><div class="result <%= 'cmd-error' if iteration.failed? %>"><%=
|
5
5
|
truncate = ::CommandProposal::Iteration::TRUNCATE_COUNT
|
6
6
|
if iteration.result.present?
|
7
7
|
iteration.result.truncate(truncate)
|
8
|
-
elsif iteration.
|
8
|
+
elsif iteration.complete?
|
9
9
|
"Error: No response"
|
10
10
|
else
|
11
11
|
content_tag :i, nil, class: "fa fa-circle-o-notch fa-spin cmd-icon-grey"
|
12
12
|
end
|
13
13
|
%></div
|
14
14
|
></div><%=
|
15
|
-
if iteration.result.
|
15
|
+
if iteration.result&.length.to_i > truncate
|
16
16
|
link_to("Output truncated. Click here to download full result.", "data:application/txt,#{ERB::Util.url_encode(iteration.result)}", class: "cmd-truncated-download", download: "result.txt")
|
17
17
|
end
|
18
18
|
%><% end -%>
|
@@ -1,5 +1,5 @@
|
|
1
1
|
<% if lines.blank? && !(skip_empty ||= false) -%><div class="line"></div><% end
|
2
|
-
-%><% lines&.split("\n")
|
2
|
+
-%><% lines&.split("\n")&.each do |line|
|
3
3
|
truncate = ::CommandProposal::Iteration::TRUNCATE_COUNT
|
4
4
|
-%><div class="line"><%= line.truncate(truncate) -%></div><%=
|
5
5
|
if line.length > truncate
|
@@ -1,8 +1,12 @@
|
|
1
|
-
<% if @task.iterations.
|
2
|
-
<table class="cmd-table">
|
1
|
+
<% if @task.iterations.any? %>
|
2
|
+
<table class="cmd-table cmd-past-iterations">
|
3
3
|
<thead>
|
4
4
|
<th>Timestamp / Link</th>
|
5
5
|
<th>Status</th>
|
6
|
+
<% if @iteration&.params.present? %>
|
7
|
+
<th>Params</th>
|
8
|
+
<% end %>
|
9
|
+
<th>Result</th>
|
6
10
|
<!-- <th># Comments</th> -->
|
7
11
|
<!-- <th>Diff</th> -->
|
8
12
|
</thead>
|
@@ -15,6 +19,14 @@
|
|
15
19
|
<%= link_to iteration.created_at.strftime("%b %-d, %Y at %H:%M"), cmd_path(@task, iteration: iteration.id) %>
|
16
20
|
</td>
|
17
21
|
<td><%= iteration.status.capitalize %></td>
|
22
|
+
<% if @iteration&.params.present? %>
|
23
|
+
<td>
|
24
|
+
<% iteration&.args.each do |arg_k, arg_v| %>
|
25
|
+
<span class=cmd-arg""><%= arg_k %>=<%= arg_v.to_s.truncate(50) %></span>
|
26
|
+
<% end %>
|
27
|
+
</td>
|
28
|
+
<% end %>
|
29
|
+
<td><%= iteration.result.to_s.truncate(200) %></td>
|
18
30
|
<!-- <td><%#= iteration.comments.count %></td> -->
|
19
31
|
<!-- <td><%#= link_to "Diff", cmd_path(@task, iteration: @iteration.id, diff: iteration.id) %></td> -->
|
20
32
|
</tr>
|
@@ -13,6 +13,7 @@ module CommandProposal
|
|
13
13
|
:approval_callback,
|
14
14
|
:success_callback,
|
15
15
|
:failed_callback,
|
16
|
+
:skip_approval_for_types,
|
16
17
|
)
|
17
18
|
|
18
19
|
def initialize
|
@@ -30,6 +31,7 @@ module CommandProposal
|
|
30
31
|
@approval_callback = nil
|
31
32
|
@success_callback = nil
|
32
33
|
@failed_callback = nil
|
34
|
+
@skip_approval_for_types = nil
|
33
35
|
end
|
34
36
|
|
35
37
|
def user_class
|
@@ -51,8 +51,21 @@ module CommandProposal
|
|
51
51
|
proposal
|
52
52
|
end
|
53
53
|
|
54
|
+
def quick_fail(iteration, msg)
|
55
|
+
@iteration = iteration
|
56
|
+
prepare
|
57
|
+
|
58
|
+
@iteration.status = :failed
|
59
|
+
@iteration.result = msg
|
60
|
+
|
61
|
+
complete
|
62
|
+
proposal = ::CommandProposal::Service::ProposalPresenter.new(@iteration)
|
63
|
+
@iteration = nil
|
64
|
+
proposal
|
65
|
+
end
|
66
|
+
|
54
67
|
def quick_run(friendly_id)
|
55
|
-
task = ::CommandProposal::Task.
|
68
|
+
task = ::CommandProposal::Task.find_by!(friendly_id: friendly_id)
|
56
69
|
iteration = task&.primary_iteration
|
57
70
|
|
58
71
|
raise CommandProposal::Error, ":#{friendly_id} does not have approval to run." unless iteration&.approved?
|
@@ -85,12 +98,12 @@ module CommandProposal
|
|
85
98
|
return @iteration.result = results_from_exception(e)
|
86
99
|
end
|
87
100
|
|
88
|
-
stored_stdout = $stdout
|
89
|
-
$stdout = StringIO.new
|
101
|
+
stored_stdout = $stdout # quiet
|
102
|
+
$stdout = StringIO.new # quiet
|
90
103
|
result = nil # Init var for scope
|
91
104
|
status = nil
|
92
105
|
|
93
|
-
|
106
|
+
runner_proc = proc {
|
94
107
|
begin
|
95
108
|
# Run `bring` functions in here so we can capture any string outputs
|
96
109
|
# OR! Run the full runner and instead of saving to an iteration, return the string for prepending here
|
@@ -102,28 +115,36 @@ module CommandProposal
|
|
102
115
|
|
103
116
|
result = results_from_exception(e)
|
104
117
|
end
|
105
|
-
|
106
|
-
|
107
|
-
while running_thread.status.present?
|
108
|
-
@iteration.reload
|
118
|
+
}
|
109
119
|
|
110
|
-
|
111
|
-
|
120
|
+
if Rails.application.config.active_job&.queue_adapter == :inline
|
121
|
+
runner_proc.call
|
122
|
+
else
|
123
|
+
running_thread = Thread.new do
|
124
|
+
runner_proc.call
|
112
125
|
end
|
113
126
|
|
114
|
-
|
115
|
-
|
116
|
-
status = :cancelled
|
117
|
-
end
|
127
|
+
while running_thread.status.present?
|
128
|
+
@iteration.reload
|
118
129
|
|
119
|
-
|
130
|
+
if $stdout.try(:string) != @iteration.result
|
131
|
+
@iteration.update(result: $stdout.try(:string).dup)
|
132
|
+
end
|
133
|
+
|
134
|
+
if @iteration.cancelling?
|
135
|
+
running_thread.exit
|
136
|
+
status = :cancelled
|
137
|
+
end
|
138
|
+
|
139
|
+
sleep 0.4
|
140
|
+
end
|
120
141
|
end
|
121
142
|
|
122
143
|
output = $stdout.try(:string)
|
123
144
|
output = nil if output == ""
|
124
145
|
# Not using presence because we want to maintain other empty objects such as [] and {}
|
125
146
|
|
126
|
-
$stdout = stored_stdout
|
147
|
+
$stdout = stored_stdout # quiet
|
127
148
|
@iteration.status = status
|
128
149
|
@iteration.result = [output, result].compact.join("\n")
|
129
150
|
end
|
@@ -13,7 +13,7 @@ module CommandProposal
|
|
13
13
|
def terminate(iteration)
|
14
14
|
return unless iteration.running?
|
15
15
|
|
16
|
-
terminated_result = iteration
|
16
|
+
terminated_result = "#{iteration&.result}\n\n~~~~~ TERMINATED ~~~~~"
|
17
17
|
iteration.update(
|
18
18
|
status: :terminated,
|
19
19
|
result: terminated_result,
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: command_proposal
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.16
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rocco Nicholls
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-03-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -162,7 +162,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
162
162
|
- !ruby/object:Gem::Version
|
163
163
|
version: '0'
|
164
164
|
requirements: []
|
165
|
-
rubygems_version: 3.2.
|
165
|
+
rubygems_version: 3.2.3
|
166
166
|
signing_key:
|
167
167
|
specification_version: 4
|
168
168
|
summary: Gives the ability to run approved commands through a UI in your browser
|