Latest Threat Research:SANDWORM_MODE: Shai-Hulud-Style npm Worm Hijacks CI Workflows and Poisons AI Toolchains.Details
Socket
Book a DemoInstallSign in
Socket

deployed

Package Overview
Dependencies
Maintainers
1
Versions
4
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

deployed - npm Package Compare versions

Comparing version
0.1.2
to
0.1.3
+67
app/controllers/deployed/log_output_controller.rb
module Deployed
class LogOutputController < ApplicationController
include ActionController::Live
before_action :set_headers
def index
thread_exit_flag = false
thread = Thread.new do
File.open(current_log_file, 'r') do |file|
while true
IO.select([file])
found_deployed = false
file.each_line do |line|
# Check the exit flag
if thread_exit_flag
break
end
css_class = if line.include?('[Deployed]')
'text-slate-400'
else
'text-green-400'
end
sse.write("<div class='#{css_class}'>#{line.strip}</div>", event: 'message')
if line.include?("[Deployed Rails] End")
found_deployed = true
break
end
end
if found_deployed || thread_exit_flag
break
end
end
end
end
begin
thread.join
rescue ActionController::Live::ClientDisconnected
logger.info 'Client Disconnected'
ensure
# Set the exit flag to true to signal the thread to exit
thread_exit_flag = true
sse.close
response.stream.close
end
end
private
def set_headers
response.headers['Content-Type'] = 'text/event-stream'
response.headers['Last-Modified'] = Time.now.httpdate
end
def sse
@sse ||= SSE.new(response.stream, event: 'Stream Started')
end
end
end
# lib/tasks/deployed.rake
namespace :deployed do
desc "Execute a Kamal command and log its output"
task :execute_and_log, [:command] => :environment do |task, args|
command = args[:command]
unless command
puts "Please provide a Kamal command. Usage: rake deployed:execute_and_log[command]"
next
end
log_file = Rails.root.join(Deployed::DIRECTORY, 'deployments/current.log')
File.open(log_file, 'a') do |file|
IO.popen("kamal #{command}") do |io|
start_time = Time.now
file.puts("[Deployed] > kamal #{command}")
file.fsync
io.each_line do |line|
file.puts line
file.fsync # Force data to be written to disk immediately
end
end_time = Time.now
file.puts("[Deployed] Finished in #{end_time - start_time} seconds")
file.puts("[Deployed] End")
file.fsync
# Delete lockfile
File.delete(Rails.root.join(Deployed::DIRECTORY, 'process.lock'))
end
end
end
end
+74
-37

@@ -7,27 +7,16 @@ import 'https://cdn.skypack.dev/@hotwired/turbo-rails'

window.execDeployed = (commandToRun) => {
// Let the frontend know we're starting
Alpine.store('process').start()
const endMarker = '[Deployed Rails] End'
const outputContainerEl = document.getElementById('deploy-output')
const spinnerEl = document.getElementById('spinner')
let outputContainerEl = document.getElementById('deploy-output')
let spinnerEl = document.getElementById('spinner')
if (outputContainerEl.innerHTML !== '') {
outputContainerEl.innerHTML += "<div class='py-2'></div>"
}
window.pipeLogs = () => {
spinnerEl.classList.remove('hidden')
var source = new EventSource(`/deployed/execute?command=${commandToRun}`)
window.logEventSource = new EventSource(`/deployed/log_output`)
source.onmessage = (event) => {
window.logEventSource.onmessage = (event) => {
if (!Alpine.store('process').running) {
source.close()
window.logEventSource.close()
} else {
if (event.data.includes('[Deployed Rails] End transmission')) {
source.close()
outputContainerEl.innerHTML += `<div class="text-slate-400 pb-4">Executed: <span class='text-slate-400 font-semibold'>kamal ${commandToRun}</span></div>`
spinnerEl.classList.add('hidden')
// Let the frontend know we're done
Alpine.store('process').stop()
if (event.data.includes("[Deployed] End")) {
window.stopPipeLogs()
} else {

@@ -43,2 +32,42 @@ outputContainerEl.innerHTML += event.data

window.stopPipeLogs = () => {
if (typeof(window.logEventSource) !== 'undefined') {
window.logEventSource.close()
}
spinnerEl.classList.add('hidden')
Alpine.store('process').stop()
}
window.execDeployed = (commandToRun) => {
Alpine.store('process').start()
let endpoint = `/deployed/execute`
// Create a data object with your payload (in this case, a command)
const data = { command: commandToRun }
// Define the fetch options for the POST request
const options = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
}
// Perform the POST request using the fetch API
fetch(endpoint, options)
.then(response => {
if (response.ok) {
outputContainerEl.innerHTML += "<div class='py-2'></div>"
outputContainerEl.innerHTML += `<div class='text-slate-400'>[Deployed] Command Received: kamal ${commandToRun}</div>`
window.pipeLogs()
return response.json(); // Parse the JSON response if needed
} else {
throw new Error('Network response was not ok');
}
})
.catch(error => {
console.error('Fetch error:', error)
})
}
window.abortDeployed = () => {

@@ -51,22 +80,30 @@ // Let the frontend know we're starting

outputContainerEl.innerHTML += `<div class="text-red-400 py-4">Aborting...</div>`
outputContainerEl.innerHTML += `<div class="text-red-400">Aborting...</div>`
var source = new EventSource(`/deployed/cancel`)
let endpoint = `/deployed/cancel`
source.onmessage = (event) => {
if (event.data.includes('[Deployed Rails] End transmission')) {
source.close()
const options = {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
}
spinnerEl.classList.add('hidden')
// Let the frontend know we're done
Alpine.store('process').stop()
Alpine.store('process').resetAbort()
} else {
outputContainerEl.innerHTML += event.data
}
outputContainerEl.scrollIntoView({ behavior: "smooth", block: "end" })
spinnerEl.scrollIntoView({ behavior: "smooth", block: "end" })
}
// Perform the POST request using the fetch API
fetch(endpoint, options)
.then(response => {
if (response.ok) {
window.stopPipeLogs()
Alpine.store('process').stop()
Alpine.store('process').resetAbort()
return response.json(); // Parse the JSON response if needed
} else {
throw new Error('Network response was not ok');
}
})
.then(data => {
console.log(data)
outputContainerEl.innerHTML += `<div class="text-yellow-400">Aborted process with PID ${data.message}</div>`
})
.catch(error => {
console.error('Fetch error:', error)
})
}

@@ -73,0 +110,0 @@

@@ -728,6 +728,2 @@ /*

.left-\[-225px\] {
left: -225px;
}
.right-0 {

@@ -783,6 +779,2 @@ right: 0px;

.ml-4 {
margin-left: 1rem;
}
.mr-2 {

@@ -828,6 +820,2 @@ margin-right: 0.5rem;

.h-11 {
height: 2.75rem;
}
.h-5 {

@@ -857,6 +845,2 @@ height: 1.25rem;

.w-11 {
width: 2.75rem;
}
.w-5 {

@@ -886,11 +870,2 @@ width: 1.25rem;

.max-w-max {
max-width: -moz-max-content;
max-width: max-content;
}
.max-w-md {
max-width: 28rem;
}
.flex-1 {

@@ -900,10 +875,2 @@ flex: 1 1 0%;

.flex-auto {
flex: 1 1 auto;
}
.flex-none {
flex: none;
}
.translate-y-0 {

@@ -914,7 +881,2 @@ --tw-translate-y: 0px;

.translate-y-1 {
--tw-translate-y: 0.25rem;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.translate-y-4 {

@@ -973,12 +935,2 @@ --tw-translate-y: 1rem;

.gap-x-1 {
-moz-column-gap: 0.25rem;
column-gap: 0.25rem;
}
.gap-x-6 {
-moz-column-gap: 1.5rem;
column-gap: 1.5rem;
}
.gap-x-8 {

@@ -989,6 +941,2 @@ -moz-column-gap: 2rem;

.gap-y-1 {
row-gap: 0.25rem;
}
.gap-y-4 {

@@ -1004,2 +952,8 @@ row-gap: 1rem;

.space-x-2 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(0.5rem * var(--tw-space-x-reverse));
margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse)));
}
.space-y-10 > :not([hidden]) ~ :not([hidden]) {

@@ -1017,14 +971,2 @@ --tw-space-y-reverse: 0;

.space-x-3 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(0.75rem * var(--tw-space-x-reverse));
margin-left: calc(0.75rem * calc(1 - var(--tw-space-x-reverse)));
}
.space-x-2 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(0.5rem * var(--tw-space-x-reverse));
margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse)));
}
.divide-y > :not([hidden]) ~ :not([hidden]) {

@@ -1057,6 +999,2 @@ --tw-divide-y-reverse: 0;

.rounded-3xl {
border-radius: 1.5rem;
}
.rounded-full {

@@ -1082,2 +1020,6 @@ border-radius: 9999px;

.border-b-4 {
border-bottom-width: 4px;
}
.border-t {

@@ -1087,10 +1029,2 @@ border-top-width: 1px;

.border-b-2 {
border-bottom-width: 2px;
}
.border-b-4 {
border-bottom-width: 4px;
}
.border-slate-200 {

@@ -1106,7 +1040,2 @@ --tw-border-opacity: 1;

.bg-gray-50 {
--tw-bg-opacity: 1;
background-color: rgb(249 250 251 / var(--tw-bg-opacity));
}
.bg-gray-500 {

@@ -1279,10 +1208,2 @@ --tw-bg-opacity: 1;

.pt-\[52px\] {
padding-top: 52px;
}
.pt-\[55px\] {
padding-top: 55px;
}
.pt-\[56px\] {

@@ -1353,7 +1274,2 @@ padding-top: 56px;

.text-gray-600 {
--tw-text-opacity: 1;
color: rgb(75 85 99 / var(--tw-text-opacity));
}
.text-gray-700 {

@@ -1444,8 +1360,2 @@ --tw-text-opacity: 1;

.shadow-lg {
--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.shadow-md {

@@ -1488,10 +1398,2 @@ --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);

.transition {
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.transition-all {

@@ -1509,6 +1411,2 @@ transition-property: all;

.duration-150 {
transition-duration: 150ms;
}
.duration-200 {

@@ -1592,12 +1490,2 @@ transition-duration: 200ms;

.group:hover .group-hover\:bg-white {
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
}
.group:hover .group-hover\:text-indigo-600 {
--tw-text-opacity: 1;
color: rgb(79 70 229 / var(--tw-text-opacity));
}
@media (min-width: 640px) {

@@ -1647,11 +1535,1 @@ .sm\:col-span-2 {

}
@media (min-width: 1024px) {
.lg\:max-w-2xl {
max-width: 42rem;
}
.lg\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}

@@ -17,3 +17,48 @@ # frozen_string_literal: true

end
def lock_file_path
Rails.root.join(Deployed::DIRECTORY, 'process.lock')
end
def lock_process
File.open(lock_file_path, 'a') do |file|
file.puts(Deployed::CurrentExecution.child_pid)
end
end
def release_process
return unless File.exist?(lock_file_path)
File.delete(lock_file_path)
end
def stored_pid
return false unless File.exist?(lock_file_path)
value = File.read(lock_file_path).to_i
if value.is_a?(Integer)
value
else
false
end
end
def process_running?
return false unless stored_pid
begin
# Send signal 0 to the process to check if it exists
Process.kill(0, stored_pid)
true
rescue Errno::ESRCH
false
end
end
helper_method :process_running?
def current_log_file
Rails.root.join(Deployed::DIRECTORY, 'deployments/current.log')
end
end
end

@@ -7,79 +7,19 @@ # frozen_string_literal: true

class ConcurrentProcessRunning < StandardError; end
skip_forgery_protection
include ActionController::Live
before_action :set_headers
# Endpoint to execute the kamal command
def execute
if process_running?
raise(ConcurrentProcessRunning)
elsif stored_pid
release_process
end
raise(ConcurrentProcessRunning) if process_running?
release_process if stored_pid
File.write(current_log_file, '')
sse.write(
"<div class='text-slate-400'>> <span class='text-slate-300 font-semibold'>kamal #{command}</span></div>",
event: 'message'
)
read_io, write_io = IO.pipe
# Fork a child process
Deployed::CurrentExecution.child_pid = fork do
# Redirect the standard output to the write end of the pipe
$stdout.reopen(write_io)
# Execute the command
exec("kamal #{command}; echo \"[Deployed Rails] End transmission\"")
exec("bundle exec rake deployed:execute_and_log['#{command}']")
end
lock_process
sse.write(
"<div class='text-slate-400' data-child-pid=\"#{Deployed::CurrentExecution.child_pid}\"></div>",
event: 'message'
)
write_io.close
# Use a separate thread to read and stream the output
output_thread = Thread.new do
read_io.each_line do |line|
output = line.strip
output = output.gsub('49.13.91.176', '[redacted]')
text_color_class = 'text-green-400'
# Hackish way of dealing with error messages at the moment
if output.include?('[31m')
text_color_class = 'text-red-500'
output.gsub!('[31m', '')
output.gsub!('[0m', '')
end
sse.write("<div class='#{text_color_class}'>#{output}</div>", event: 'message')
end
# Ensure the response stream and the thread are closed properly
sse.close
response.stream.close
end
# Ensure that the thread is joined when the execution is complete
Process.wait
output_thread.join
render json: { message: 'OK' }
rescue ConcurrentProcessRunning
sse.write(
"<div class='text-red-500'>Existing process running with PID: #{stored_pid}</div>",
event: 'message'
)
logger.info 'Existing process running'
rescue ActionController::Live::ClientDisconnected
logger.info 'Client Disconnected'
rescue IOError
logger.info 'IOError'
ensure
sse.close
response.stream.close
release_process
render json: { message: 'EXISTING PROCESS' }
end

@@ -89,2 +29,3 @@

def cancel
pid = stored_pid
if process_running?

@@ -94,31 +35,8 @@ # If a process is running, get the PID and attempt to kill it

Process.kill('TERM', stored_pid)
sse.write(
"<div class='text-yellow-400'>Cancelled the process with PID: #{stored_pid}</div>",
event: 'message'
)
rescue Errno::ESRCH
ensure
release_process
rescue Errno::ESRCH
sse.write(
"<div class='text-red-500'>Process with PID #{stored_pid} is not running.</div>",
event: 'message'
)
end
else
sse.write(
"<div class='text-slate-400'>No process is currently running, nothing to cancel.</div>",
event: 'message'
)
end
rescue ActionController::Live::ClientDisconnected
logger.info 'Client Disconnected'
rescue IOError
logger.info 'IOError'
ensure
sse.write(
'[Deployed Rails] End transmission',
event: 'message'
)
sse.close
response.stream.close
release_process
render json: { message: pid }
end

@@ -128,55 +46,6 @@

def set_headers
response.headers['Content-Type'] = 'text/event-stream'
response.headers['Last-Modified'] = Time.now.httpdate
end
def sse
@sse ||= SSE.new(response.stream, event: 'Stream Started')
end
def command
params[:command]
end
def lock_file_path
Rails.root.join(Deployed::DIRECTORY, 'process.lock')
end
def lock_process
File.open(lock_file_path, 'a') do |file|
file.puts(Deployed::CurrentExecution.child_pid)
end
end
def release_process
return unless File.exist?(lock_file_path)
File.delete(lock_file_path)
end
def stored_pid
return false unless File.exist?(lock_file_path)
value = File.read(lock_file_path).to_i
if value.is_a?(Integer)
value
else
false
end
end
def process_running?
return false unless stored_pid
begin
# Send signal 0 to the process to check if it exists
Process.kill(0, stored_pid)
true
rescue Errno::ESRCH
false
end
end
end
end

@@ -13,3 +13,3 @@ <!DOCTYPE html>

Alpine.store('process', {
running: false,
running: <%= process_running? %>,
start() {

@@ -51,10 +51,8 @@ this.running = true

</div>
<div>
<span class="text-base text-slate-400">v<%= Deployed::VERSION %></span>
</div>
</div>
</h1>
</div>
<% if false %>
<div class="ml-4">
<%= render 'layouts/deployed/nav_menu' %>
</div>
<% end %>
</div>

@@ -91,3 +89,10 @@ </header>

<%= turbo_frame_tag('deployed-init', src: setup_path, target: '_top') if Deployed::Config.requires_init %>
<!-- If we refresh the page, we want to see the logs still piping... -->
<script>
setTimeout(() => {
<% if process_running? %>window.pipeLogs()<% end %>
}, 1000)
</script>
</body>
</html>

@@ -6,5 +6,6 @@ Deployed::Engine.routes.draw do

get 'git/uncommitted_check', to: 'git#uncommitted_check'
get 'execute', to: 'run#execute'
get 'cancel', to: 'run#cancel'
post 'execute', to: 'run#execute'
post 'cancel', to: 'run#cancel'
get 'log_output', to: 'log_output#index'
root to: 'welcome#index'
end
module Deployed
VERSION = "0.1.2"
VERSION = "0.1.3"
end
+29
-12
# Deployed
[![Gem Version](https://badge.fury.io/rb/deployed.svg)](https://badge.fury.io/rb/deployed)
Deployed is a web interface for the deployment library, [Kamal](https://kamal-deploy.org).
## Usage
How to use my plugin.
Here is a quick video demo: https://x.com/geetfun/status/1716109581619744781?s=20
## Requirements
Ruby on Rails
## Installation

@@ -12,19 +17,31 @@ Add this line to your application's Gemfile:

```ruby
gem "deployed"
group :development do
gem 'kamal'
gem 'deployed'
end
```
And then execute:
```bash
$ bundle
```
## Usage
Or install it yourself as:
```bash
$ gem install deployed
Add the following to your app's routes file:
```ruby
Rails.application.routes.draw do
if Rails.env.development? && defined?(Deployed)
mount(Deployed::Engine => '/deployed')
end
# Your other routes...
end
```
## Contributing
Contribution directions go here.
Next, head to `http://localhost:3000/deployed`
## Development
Run `bin/setup` to bootstrap the development environment.
To run tests: `bundle exec rake app:test`. Currently there are no tests, but some will be added soon.
## License
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
<div class="relative">
<button type="button" class="inline-flex items-center gap-x-1 text-sm font-semibold leading-6 text-slate-400" aria-expanded="false">
<span>Resources</span>
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd" />
</svg>
</button>
<!--
Flyout menu, show/hide based on flyout menu state.
Entering: "transition ease-out duration-200"
From: "opacity-0 translate-y-1"
To: "opacity-100 translate-y-0"
Leaving: "transition ease-in duration-150"
From: "opacity-100 translate-y-0"
To: "opacity-0 translate-y-1"
-->
<div class="absolute z-10 mt-5 flex w-screen max-w-max left-[-225px] px-4">
<div class="w-screen max-w-md flex-auto overflow-hidden rounded-3xl bg-white text-sm leading-6 shadow-lg ring-1 ring-gray-900/5 lg:max-w-2xl">
<div class="grid grid-cols-1 gap-x-6 gap-y-1 p-4 lg:grid-cols-2">
<div class="group relative flex gap-x-6 rounded-lg p-4 hover:bg-gray-50">
<div class="mt-1 flex h-11 w-11 flex-none items-center justify-center rounded-lg bg-gray-50 group-hover:bg-white">
<svg class="h-6 w-6 text-gray-600 group-hover:text-indigo-600" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 6a7.5 7.5 0 107.5 7.5h-7.5V6z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 10.5H21A7.5 7.5 0 0013.5 3v7.5z" />
</svg>
</div>
<div>
<a href="#" class="font-semibold text-gray-900">
Documentation
<span class="absolute inset-0"></span>
</a>
<p class="mt-1 text-gray-600">Check out our documentation</p>
</div>
</div>
<div class="group relative flex gap-x-6 rounded-lg p-4 hover:bg-gray-50">
<div class="mt-1 flex h-11 w-11 flex-none items-center justify-center rounded-lg bg-gray-50 group-hover:bg-white">
<svg class="h-6 w-6 text-gray-600 group-hover:text-indigo-600" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 16.875h3.375m0 0h3.375m-3.375 0V13.5m0 3.375v3.375M6 10.5h2.25a2.25 2.25 0 002.25-2.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v2.25A2.25 2.25 0 006 10.5zm0 9.75h2.25A2.25 2.25 0 0010.5 18v-2.25a2.25 2.25 0 00-2.25-2.25H6a2.25 2.25 0 00-2.25 2.25V18A2.25 2.25 0 006 20.25zm9.75-9.75H18a2.25 2.25 0 002.25-2.25V6A2.25 2.25 0 0018 3.75h-2.25A2.25 2.25 0 0013.5 6v2.25a2.25 2.25 0 002.25 2.25z" />
</svg>
</div>
<div>
<a href="#" class="font-semibold text-gray-900">
Our GitHub Repository
<span class="absolute inset-0"></span>
</a>
<p class="mt-1 text-gray-600">Ipsum Lorem</p>
</div>
</div>
</div>
</div>
</div>
</div>
# desc "Explaining what the task does"
# task :deployed do
# # Task goes here
# end