command_proposal 1.0.13 → 1.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/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
|