kitsune-kit 0.1.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.
@@ -0,0 +1,189 @@
1
+ require "thor"
2
+ require "net/ssh"
3
+ require_relative "../defaults"
4
+ require_relative "../options_builder"
5
+
6
+ module Kitsune
7
+ module Kit
8
+ module Commands
9
+ class SetupUser < Thor
10
+ namespace "setup_user"
11
+
12
+ class_option :server_ip, aliases: "-s", required: true, desc: "Server IP address or hostname"
13
+ class_option :ssh_port, aliases: "-p", desc: "SSH port"
14
+ class_option :ssh_key_path, aliases: "-k", desc: "Path to your private SSH key"
15
+
16
+ desc "create", "Create and configure 'deploy' user on remote server"
17
+ def create
18
+ filled_options = Kitsune::Kit::OptionsBuilder.build(
19
+ options,
20
+ required: [:server_ip],
21
+ defaults: Kitsune::Kit::Defaults.ssh
22
+ )
23
+
24
+ with_ssh_connection(false, filled_options) do |ssh|
25
+ perform_setup(ssh)
26
+ end
27
+ end
28
+
29
+ desc "rollback", "Revert configuration and remove 'deploy' user from remote server"
30
+ def rollback
31
+ filled_options = Kitsune::Kit::OptionsBuilder.build(
32
+ options,
33
+ required: [:server_ip],
34
+ defaults: Kitsune::Kit::Defaults.ssh
35
+ )
36
+
37
+ server = filled_options[:server_ip]
38
+ port = filled_options[:ssh_port]
39
+ key = File.expand_path(filled_options[:ssh_key_path])
40
+
41
+ # First, attempt SSH config restore as 'deploy'
42
+ begin
43
+ with_ssh_connection(true, filled_options) do |ssh|
44
+ perform_rollback_config(ssh)
45
+ end
46
+ rescue StandardError => e
47
+ say "⚠️ Skipping SSH config restore: #{e.message}", :yellow
48
+ end
49
+
50
+ # Then reconnect as 'root' to remove sudoers and delete user
51
+ say "πŸ”‘ Reconnecting as root@#{server}:#{port}", :green
52
+ Net::SSH.start(server, 'root', port: port, keys: [key], non_interactive: true) do |ssh|
53
+ perform_rollback_cleanup(ssh)
54
+ end
55
+ say "βœ… Rollback completed", :green
56
+ end
57
+
58
+ no_commands do
59
+ def with_ssh_connection(rollback, filled_options)
60
+ server = filled_options[:server_ip]
61
+ port = filled_options[:ssh_port]
62
+ key = File.expand_path(filled_options[:ssh_key_path])
63
+
64
+ user = rollback ? 'deploy' : detect_remote_user(server, port, key)
65
+ say "πŸ”‘ Connecting as #{user}@#{server}:#{port}", :green
66
+
67
+ Net::SSH.start(server, user, port: port, keys: [key], non_interactive: true, timeout: 5) do |ssh|
68
+ yield ssh
69
+ end
70
+ end
71
+
72
+ def detect_remote_user(server, port, key)
73
+ %w[deploy root].each do |u|
74
+ begin
75
+ Net::SSH.start(server, u, port: port, keys: [key], non_interactive: true, timeout: 5) { }
76
+ say "βœ”οΈ Able to SSH as #{u}", :green
77
+ return u
78
+ rescue
79
+ next
80
+ end
81
+ end
82
+ abort("❌ Could not connect as deploy or root on #{server}:#{port}")
83
+ end
84
+
85
+ def perform_setup(ssh)
86
+ output = ssh.exec! <<~'EOH'
87
+ set -e
88
+
89
+ echo "✍🏻 Creating deploy user…"
90
+ if ! id deploy &>/dev/null; then
91
+ if command -v adduser &>/dev/null; then
92
+ sudo adduser --disabled-password --gecos "" deploy && echo " - user 'deploy' created"
93
+ else
94
+ sudo useradd -m -s /bin/bash deploy && echo " - user 'deploy' created"
95
+ fi
96
+ sudo usermod -aG sudo deploy && echo " - 'deploy' added to sudo"
97
+ else
98
+ echo " - user 'deploy' already exists"
99
+ fi
100
+
101
+ echo "✍🏻 Configuring passwordless sudo…"
102
+ if [ ! -f /etc/sudoers.d/deploy ]; then
103
+ echo 'deploy ALL=(ALL) NOPASSWD:ALL' | sudo tee /etc/sudoers.d/deploy \
104
+ && sudo chmod 440 /etc/sudoers.d/deploy \
105
+ && echo " - sudoers entry created"
106
+ else
107
+ echo " - sudoers entry exists"
108
+ fi
109
+
110
+ echo "✍🏻 Backing up SSH config…"
111
+ sudo test -f /etc/ssh/sshd_config.bak || sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak && echo " - sshd_config backed up"
112
+
113
+ echo "✍🏻 Hardening SSH…"
114
+ grep -q '^PermitRootLogin no' /etc/ssh/sshd_config \
115
+ || sudo sed -i 's/^#*PermitRootLogin .*/PermitRootLogin no/' /etc/ssh/sshd_config && echo " - PermitRootLogin no"
116
+ grep -q '^PasswordAuthentication no' /etc/ssh/sshd_config \
117
+ || sudo sed -i 's/^#*PasswordAuthentication .*/PasswordAuthentication no/' /etc/ssh/sshd_config && echo " - PasswordAuthentication no"
118
+ sudo systemctl restart sshd && echo " - sshd restarted"
119
+
120
+ echo "✍🏻 Installing SSH keys for deploy…"
121
+ if [ ! -f /home/deploy/.ssh/authorized_keys ]; then
122
+ sudo mkdir -p /home/deploy/.ssh
123
+ sudo cp /root/.ssh/authorized_keys /home/deploy/.ssh/authorized_keys
124
+ sudo chown -R deploy:deploy /home/deploy/.ssh
125
+ sudo chmod 700 /home/deploy/.ssh
126
+ sudo chmod 600 /home/deploy/.ssh/authorized_keys
127
+ echo " - authorized_keys copied"
128
+ else
129
+ echo " - authorized_keys already present"
130
+ fi
131
+ EOH
132
+ say output
133
+ say "βœ… Setup completed", :green
134
+ end
135
+
136
+ def perform_rollback_config(ssh)
137
+ output = ssh.exec! <<~'EOH'
138
+ set -e
139
+
140
+ echo "πŸ” Backing up SSH config…"
141
+ sudo test -f /etc/ssh/sshd_config.bak || sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak && echo " - sshd_config backed up"
142
+
143
+ echo "✍🏻 Restoring SSH config…"
144
+ grep -q '^PermitRootLogin yes' /etc/ssh/sshd_config \
145
+ || sudo sed -i 's/^#*PermitRootLogin .*/PermitRootLogin yes/' /etc/ssh/sshd_config && echo " - PermitRootLogin yes"
146
+ grep -q '^PasswordAuthentication yes' /etc/ssh/sshd_config \
147
+ || sudo sed -i 's/^#*PasswordAuthentication .*/PasswordAuthentication yes/' /etc/ssh/sshd_config && echo " - PasswordAuthentication yes"
148
+ sudo systemctl restart sshd && echo " - sshd restarted"
149
+ EOH
150
+ say output
151
+ say "βœ… SSH config restored, closing deploy session", :green
152
+ end
153
+
154
+ def perform_rollback_cleanup(ssh)
155
+ output = ssh.exec! <<~'EOH'
156
+ set -e
157
+
158
+ echo "✍🏻 Removing sudoers file…"
159
+ if [ -f /etc/sudoers.d/deploy ]; then
160
+ sudo rm -f /etc/sudoers.d/deploy && echo " - /etc/sudoers.d/deploy removed"
161
+ else
162
+ echo " - no sudoers file to remove"
163
+ fi
164
+
165
+ echo "✍🏻 Killing remaining processes for deploy…"
166
+ if id deploy &>/dev/null; then
167
+ sudo pkill -u deploy && echo " - processes killed" || echo " - no processes found"
168
+ else
169
+ echo " - user 'deploy' does not exist, skipping"
170
+ fi
171
+
172
+ echo "✍🏻 Deleting deploy user…"
173
+ if id deploy &>/dev/null; then
174
+ if command -v deluser &>/dev/null; then
175
+ sudo deluser --remove-home deploy && echo " - deploy user removed"
176
+ else
177
+ sudo userdel -r deploy && echo " - deploy user removed"
178
+ fi
179
+ else
180
+ echo " - deploy user does not exist"
181
+ fi
182
+ EOH
183
+ say output
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,42 @@
1
+ require "thor"
2
+ require "fileutils"
3
+
4
+ module Kitsune
5
+ module Kit
6
+ module Commands
7
+ class SwitchEnv < Thor
8
+ namespace "switch_env"
9
+
10
+ desc "to ENV_NAME", "Switch active Kitsune Kit environment (development, production, etc)"
11
+ def to(env_name)
12
+ kit_env_path = ".kitsune/kit.env"
13
+ infra_env_path = ".kitsune/infra.#{env_name}.env"
14
+ blueprint_path = File.expand_path("../../blueprints/.env.template", __dir__)
15
+
16
+ unless File.exist?(kit_env_path)
17
+ say "❌ No .kitsune/kit.env found. Did you run `kitsune kit init`?", :red
18
+ exit(1)
19
+ end
20
+
21
+ content = File.read(kit_env_path)
22
+
23
+ if content.match?(/^KIT_ENV=/)
24
+ new_content = content.gsub(/^KIT_ENV=.*/, "KIT_ENV=#{env_name}")
25
+ else
26
+ new_content = "KIT_ENV=#{env_name}\n" + content
27
+ end
28
+
29
+ File.write(kit_env_path, new_content)
30
+ say "🎯 Environment switched to '#{env_name}' in .kitsune/kit.env", :green
31
+
32
+ unless File.exist?(infra_env_path)
33
+ FileUtils.cp(blueprint_path, infra_env_path)
34
+ say "πŸ“ Created new infra environment file: #{infra_env_path}", :cyan
35
+ else
36
+ say "πŸ“„ Infra environment file already exists: #{infra_env_path}", :green
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,56 @@
1
+ module Kitsune
2
+ module Kit
3
+ module Defaults
4
+ DROPLET = {
5
+ droplet_name: "app-prod",
6
+ region: "sfo3",
7
+ size: "s-1vcpu-1gb",
8
+ image: "ubuntu-22-04-x64",
9
+ tag: "rails-prod"
10
+ }.freeze
11
+
12
+ SSH = {
13
+ ssh_port: "22",
14
+ ssh_key_path: "~/.ssh/id_rsa"
15
+ }.freeze
16
+
17
+ POSTGRES = {
18
+ db_prefix: "myapp_db",
19
+ user: "postgres",
20
+ password: "secret",
21
+ port: "5432",
22
+ image: "postgres:15"
23
+ }.freeze
24
+
25
+ def self.infra
26
+ {
27
+ droplet_name: ENV.fetch('DROPLET_NAME', DROPLET[:droplet_name]),
28
+ region: ENV.fetch('REGION', DROPLET[:region]),
29
+ size: ENV.fetch('SIZE', DROPLET[:size]),
30
+ image: ENV.fetch('IMAGE', DROPLET[:image]),
31
+ tag: ENV.fetch('TAG_NAME', DROPLET[:tag]),
32
+ ssh_key_id: ENV.fetch('SSH_KEY_ID') { abort "❌ Missing SSH_KEY_ID" }
33
+ }
34
+ end
35
+
36
+ def self.ssh
37
+ {
38
+ ssh_port: ENV.fetch('SSH_PORT', SSH[:ssh_port]),
39
+ ssh_key_path: ENV.fetch('SSH_KEY_PATH', SSH[:ssh_key_path])
40
+ }
41
+ end
42
+
43
+ def self.postgres
44
+ env = ENV.fetch('KIT_ENV', 'development')
45
+
46
+ {
47
+ postgres_db: ENV.fetch('POSTGRES_DB') { "#{POSTGRES[:db_prefix]}_#{env}" },
48
+ postgres_user: ENV.fetch('POSTGRES_USER', POSTGRES[:user]),
49
+ postgres_password: ENV.fetch('POSTGRES_PASSWORD', POSTGRES[:password]),
50
+ postgres_port: ENV.fetch('POSTGRES_PORT', POSTGRES[:port]),
51
+ postgres_image: ENV.fetch('POSTGRES_IMAGE', POSTGRES[:image])
52
+ }
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,41 @@
1
+ require "dotenv"
2
+
3
+ module Kitsune
4
+ module Kit
5
+ class EnvLoader
6
+ @loaded = false
7
+
8
+ def self.load!
9
+ return if @loaded
10
+
11
+ env = ENV["KIT_ENV"] || read_kit_env || "development"
12
+
13
+ possible_paths = [
14
+ ".kitsune/infra.#{env}.env",
15
+ ".kitsune/infra.env"
16
+ ]
17
+
18
+ found = possible_paths.find { |path| File.exist?(path) }
19
+
20
+ if found
21
+ Dotenv.load(found)
22
+ puts "πŸ§ͺ Loaded Kitsune environment from #{found}"
23
+ else
24
+ puts "⚠️ No Kitsune infra config found for environment '#{env}' (looked for infra.#{env}.env and infra.env)"
25
+ end
26
+
27
+ @loaded = true
28
+ end
29
+
30
+ def self.read_kit_env
31
+ path = ".kitsune/kit.env"
32
+ if File.exist?(path)
33
+ vars = Dotenv.parse(path)
34
+ vars["KIT_ENV"]
35
+ else
36
+ nil
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,26 @@
1
+ module Kitsune
2
+ module Kit
3
+ class OptionsBuilder
4
+ def self.build(current_options, required: [], defaults: {})
5
+ current = current_options.transform_keys(&:to_sym)
6
+
7
+ filled = defaults.dup
8
+
9
+ defaults.keys.each do |key|
10
+ env_key = key.to_s.upcase
11
+ filled[key] = ENV[env_key] if ENV[env_key]
12
+ end
13
+
14
+ filled.merge!(current)
15
+
16
+ missing = required.select { |key| filled[key].nil? }
17
+
18
+ unless missing.empty?
19
+ abort "❌ Missing required options: #{missing.join(', ')}"
20
+ end
21
+
22
+ filled
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,109 @@
1
+ require "droplet_kit"
2
+
3
+ module Kitsune
4
+ module Kit
5
+ class Provisioner
6
+ def initialize(opts)
7
+ @droplet_name = opts[:droplet_name]
8
+ @region = opts[:region]
9
+ @size = opts[:size]
10
+ @image = opts[:image]
11
+ @tag = opts[:tag]
12
+ @ssh_key_id = opts[:ssh_key_id] do
13
+ abort "❌ You must export SSH_KEY_ID or use --ssh_key_id"
14
+ end
15
+
16
+ @client = DropletKit::Client.new(access_token: ENV.fetch("DO_API_TOKEN"))
17
+ end
18
+
19
+ # Find an existing droplet by name
20
+ def find_droplet
21
+ @client.droplets.all(tag_name: @tag).detect { |d| d.name == @droplet_name }
22
+ end
23
+
24
+ # Create command: shows if it exists or creates a new one
25
+ def create_or_show
26
+ if (d = find_droplet)
27
+ ip = public_ip(d)
28
+ puts "βœ… Droplet '#{@droplet_name}' already exists (ID: #{d.id}, IP: #{ip})"
29
+ else
30
+ puts "✍🏻 Creating Droplet '#{@droplet_name}'..."
31
+ spec = DropletKit::Droplet.new(
32
+ name: @droplet_name,
33
+ region: @region,
34
+ size: @size,
35
+ image: @image,
36
+ ssh_keys: [@ssh_key_id],
37
+ tags: [@tag]
38
+ )
39
+ created = @client.droplets.create(spec)
40
+ wait_for_status(created.id)
41
+ ip = wait_for_public_ip(created.id)
42
+
43
+ wait_for_ssh(ip)
44
+
45
+ puts "βœ… Droplet created: ID=#{created.id}, IP=#{ip}"
46
+ end
47
+ end
48
+
49
+ # Rollback command: deletes it if it exists
50
+ def rollback
51
+ if (d = find_droplet)
52
+ puts "πŸ” Deleting Droplet '#{@droplet_name}' (ID: #{d.id})..."
53
+ @client.droplets.delete(id: d.id)
54
+ puts "βœ… Droplet deleted πŸ’₯"
55
+ else
56
+ puts "βœ… Nothing to delete: '#{@droplet_name}' does not exist"
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ # Extracts the public IP from a DropletKit::Droplet
63
+ def public_ip(droplet)
64
+ v4 = droplet.networks.v4.find { |n| n.type == "public" }
65
+ v4 ? v4.ip_address : "(no public IP yet)"
66
+ end
67
+
68
+ # Waits until the droplet reaches the 'active' status
69
+ def wait_for_status(droplet_id, interval: 5, max_attempts: 24)
70
+ max_attempts.times do |i|
71
+ droplet = @client.droplets.find(id: droplet_id)
72
+ estado = droplet.status
73
+ puts "⏳ Droplet status: #{estado} (#{i + 1}/#{max_attempts})"
74
+ return if estado == "active"
75
+ sleep interval
76
+ end
77
+ abort "❌ Timeout: the Droplet did not reach 'active' status after #{interval * max_attempts} seconds"
78
+ end
79
+
80
+ # Waits until obtaining the public IP
81
+ def wait_for_public_ip(droplet_id, interval: 5, max_attempts: 24)
82
+ max_attempts.times do |i|
83
+ droplet = @client.droplets.find(id: droplet_id)
84
+ if (v4 = droplet.networks.v4.find { |n| n.type == "public" })
85
+ return v4.ip_address
86
+ end
87
+ puts "⏳ Waiting for public IP... (#{i + 1}/#{max_attempts})"
88
+ sleep interval
89
+ end
90
+ abort "❌ Timeout: the Droplet did not obtain a public IP after #{interval * max_attempts} seconds"
91
+ end
92
+
93
+ # Waits for the SSH port to become accessible
94
+ def wait_for_ssh(ip, interval: 5, max_attempts: 24)
95
+ max_attempts.times do |i|
96
+ begin
97
+ puts "πŸ” Waiting for SSH connection to #{ip}... (#{i + 1}/#{max_attempts})"
98
+ TCPSocket.new(ip, 22).close
99
+ puts "πŸ”“ SSH connection to #{ip} established"
100
+ return
101
+ rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, SocketError
102
+ sleep interval
103
+ end
104
+ end
105
+ abort "❌ Could not connect via SSH to #{ip} after #{interval * max_attempts} seconds"
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kitsune
4
+ module Kit
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "kit/version"
4
+
5
+ module Kitsune
6
+ module Kit
7
+ class Error < StandardError; end
8
+ # Your code goes here...
9
+ end
10
+ end
@@ -0,0 +1,6 @@
1
+ module Kitsune
2
+ module Kit
3
+ VERSION: String
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end
6
+ end
metadata ADDED
@@ -0,0 +1,180 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kitsune-kit
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Omar Herrera
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-04-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: net-ssh
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: ed25519
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bcrypt_pbkdf
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: dotenv
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: droplet_kit
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: thor
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: pry
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ description: Kitsune Kit is a CLI toolkit that automates the provisioning, configuration,
112
+ and Docker setup of remote servers, especially tailored for Ruby developers using
113
+ Kamal. Includes rollback features and multi-environment support.
114
+ email:
115
+ - contact@omarherrera.me
116
+ executables:
117
+ - kit
118
+ extensions: []
119
+ extra_rdoc_files: []
120
+ files:
121
+ - ".rspec"
122
+ - CHANGELOG.md
123
+ - CODE_OF_CONDUCT.md
124
+ - LICENSE.txt
125
+ - README.md
126
+ - Rakefile
127
+ - bin/kit
128
+ - kitsune-kit-logo.jpg
129
+ - lib/kitsune/blueprints/.env.template
130
+ - lib/kitsune/blueprints/docker/postgres.yml
131
+ - lib/kitsune/blueprints/kit.env.template
132
+ - lib/kitsune/kit.rb
133
+ - lib/kitsune/kit/cli.rb
134
+ - lib/kitsune/kit/commands/bootstrap.rb
135
+ - lib/kitsune/kit/commands/bootstrap_docker.rb
136
+ - lib/kitsune/kit/commands/init.rb
137
+ - lib/kitsune/kit/commands/install_docker_engine.rb
138
+ - lib/kitsune/kit/commands/postinstall_docker.rb
139
+ - lib/kitsune/kit/commands/provision.rb
140
+ - lib/kitsune/kit/commands/setup_docker_prereqs.rb
141
+ - lib/kitsune/kit/commands/setup_firewall.rb
142
+ - lib/kitsune/kit/commands/setup_postgres_docker.rb
143
+ - lib/kitsune/kit/commands/setup_unattended.rb
144
+ - lib/kitsune/kit/commands/setup_user.rb
145
+ - lib/kitsune/kit/commands/switch_env.rb
146
+ - lib/kitsune/kit/defaults.rb
147
+ - lib/kitsune/kit/env_loader.rb
148
+ - lib/kitsune/kit/options_builder.rb
149
+ - lib/kitsune/kit/provisioner.rb
150
+ - lib/kitsune/kit/version.rb
151
+ - sig/kitsune/kit.rbs
152
+ homepage: https://github.com/omarhrra/kitsune-kit
153
+ licenses:
154
+ - MIT
155
+ metadata:
156
+ allowed_push_host: https://rubygems.org
157
+ homepage_uri: https://github.com/omarhrra/kitsune-kit
158
+ source_code_uri: https://github.com/omarhrra/kitsune-kit
159
+ changelog_uri: https://github.com/omarhrra/kitsune-kit/blob/main/CHANGELOG.md
160
+ post_install_message:
161
+ rdoc_options: []
162
+ require_paths:
163
+ - lib
164
+ required_ruby_version: !ruby/object:Gem::Requirement
165
+ requirements:
166
+ - - ">="
167
+ - !ruby/object:Gem::Version
168
+ version: 3.0.0
169
+ required_rubygems_version: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ requirements: []
175
+ rubygems_version: 3.5.22
176
+ signing_key:
177
+ specification_version: 4
178
+ summary: Provision and setup DigitalOcean VPSs with Docker and PostgreSQL, ideal for
179
+ Kamal deployments.
180
+ test_files: []