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,148 @@
1
+ require "thor"
2
+ require "fileutils"
3
+
4
+ module Kitsune
5
+ module Kit
6
+ module Commands
7
+ class Init < Thor
8
+ namespace "init"
9
+
10
+ default_task :init
11
+
12
+ desc "init", "Initialize Kitsune Kit project structure"
13
+ def init
14
+ say "✨ Initializing Kitsune project...", :green
15
+ create_base_structure
16
+ selected_envs = select_environments
17
+ copy_env_templates(selected_envs)
18
+ selected_default_env = select_default_environment(selected_envs)
19
+ create_kit_env(selected_default_env)
20
+ copy_docker_templates
21
+ say "🎉 Done! '.kitsune/' structure is ready.", :green
22
+ end
23
+
24
+ no_commands do
25
+ def blueprint_path(relative_path)
26
+ File.expand_path("../../blueprints/#{relative_path}", __dir__)
27
+ end
28
+
29
+ def create_base_structure
30
+ dirs = [
31
+ ".kitsune",
32
+ ".kitsune/docker"
33
+ ]
34
+ dirs.each do |dir|
35
+ unless Dir.exist?(dir)
36
+ FileUtils.mkdir_p(dir)
37
+ say "📂 Created directory: #{dir}", :cyan
38
+ else
39
+ say "📂 Directory already exists: #{dir}", :yellow
40
+ end
41
+ end
42
+ end
43
+
44
+ def environments_options
45
+ {
46
+ "1" => "development",
47
+ "2" => "production",
48
+ "3" => "staging",
49
+ "4" => "test"
50
+ }
51
+ end
52
+
53
+ def select_environments
54
+ say "🌎 Which environments do you want to create?", :cyan
55
+ environments_options.each do |number, env|
56
+ say " #{number}) #{env}"
57
+ end
58
+ input = ask("➡️ Enter numbers separated by commas (or type 'all') [default: all]:", :yellow)
59
+ input = input.strip.downcase
60
+
61
+ if input.empty? || input == "all"
62
+ environments_options.values
63
+ else
64
+ selected = input.split(",").map(&:strip)
65
+ environments = selected.map { |num| environments_options[num] }.compact
66
+ if environments.empty?
67
+ say "⚠️ Invalid selection. Creating all environments.", :yellow
68
+ environments_options.values
69
+ else
70
+ environments
71
+ end
72
+ end
73
+ end
74
+
75
+ def select_default_environment(selected_envs)
76
+ say "🎯 Which environment should be set as default in '.kitsune/kit.env'?", :cyan
77
+ selected_envs.each_with_index do |env, index|
78
+ say " #{index + 1}) #{env}"
79
+ end
80
+ input = ask("➡️ Enter number [default: 1]:", :yellow)
81
+ input = input.strip
82
+
83
+ if input.empty?
84
+ selected_envs[0] # default to first selected
85
+ else
86
+ index = input.to_i - 1
87
+ if index >= 0 && index < selected_envs.size
88
+ selected_envs[index]
89
+ else
90
+ say "⚠️ Invalid selection. Defaulting to '#{selected_envs[0]}'", :yellow
91
+ selected_envs[0]
92
+ end
93
+ end
94
+ end
95
+
96
+ def create_kit_env(default_env)
97
+ path = ".kitsune/kit.env"
98
+ template = File.read(blueprint_path("kit.env.template"))
99
+ content = template.gsub("KIT_ENV=development", "KIT_ENV=#{default_env}")
100
+
101
+ if File.exist?(path)
102
+ if yes?("⚠️ File #{path} already exists. Overwrite? [y/N]", :yellow)
103
+ File.write(path, content)
104
+ say "✅ Overwritten: #{path}", :cyan
105
+ else
106
+ say "⏩ Skipped: #{path}", :yellow
107
+ end
108
+ else
109
+ File.write(path, content)
110
+ say "📝 Created: #{path}", :cyan
111
+ end
112
+
113
+ say ""
114
+ say "🎯 Kitsune Kit environment set to: #{default_env}", :green
115
+ say "📄 Environment file used: .kitsune/infra.#{default_env}.env", :green
116
+ say ""
117
+ end
118
+
119
+ def copy_env_templates(selected_envs)
120
+ selected_envs.each do |env|
121
+ dest_path = ".kitsune/infra.#{env}.env"
122
+ copy_with_prompt(blueprint_path(".env.template"), dest_path)
123
+ end
124
+ end
125
+
126
+ def copy_docker_templates
127
+ dest_path = ".kitsune/docker/postgres.yml"
128
+ copy_with_prompt(blueprint_path("docker/postgres.yml"), dest_path)
129
+ end
130
+
131
+ def copy_with_prompt(source, destination)
132
+ if File.exist?(destination)
133
+ if yes?("⚠️ File #{destination} already exists. Overwrite? [y/N]", :yellow)
134
+ FileUtils.cp(source, destination)
135
+ say "✅ Overwritten: #{destination}", :cyan
136
+ else
137
+ say "⏩ Skipped: #{destination}", :yellow
138
+ end
139
+ else
140
+ FileUtils.cp(source, destination)
141
+ say "📝 Created: #{destination}", :cyan
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,146 @@
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 InstallDockerEngine < Thor
10
+ namespace "install_docker_engine"
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", "Install Docker Engine on the 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(filled_options) do |ssh|
25
+ perform_setup(ssh)
26
+ end
27
+ end
28
+
29
+ desc "rollback", "Uninstall Docker Engine from the 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
+ with_ssh_connection(filled_options) do |ssh|
38
+ perform_rollback(ssh)
39
+ end
40
+ end
41
+
42
+ no_commands do
43
+ def with_ssh_connection(filled_options)
44
+ server = filled_options[:server_ip]
45
+ port = filled_options[:ssh_port]
46
+ key = File.expand_path(filled_options[:ssh_key_path])
47
+
48
+ say "🔑 Connecting as deploy@#{server}:#{port}", :green
49
+ Net::SSH.start(server, "deploy", port: port, keys: [key], non_interactive: true, timeout: 5) do |ssh|
50
+ yield ssh
51
+ end
52
+ end
53
+
54
+ def perform_setup(ssh)
55
+ output = ssh.exec! <<~EOH
56
+ set -e
57
+
58
+ sudo mkdir -p /usr/local/backups
59
+ sudo chown deploy:deploy /usr/local/backups
60
+
61
+ BACKUP_DIR="/usr/local/backups"
62
+ SCRIPT_ID="install_docker_engine"
63
+ BEFORE_FILE="${BACKUP_DIR}/${SCRIPT_ID}.before"
64
+ AFTER_FILE="${BACKUP_DIR}/${SCRIPT_ID}.after"
65
+
66
+ TARGET_PKGS=(docker-ce docker-ce-cli containerd.io)
67
+
68
+ echo "✍🏻 TARGET_PKGS=(\${TARGET_PKGS[*]})"
69
+
70
+ if [ ! -f "$AFTER_FILE" ]; then
71
+ for pkg in "\${TARGET_PKGS[@]}"; do
72
+ if dpkg -l "\$pkg" &>/dev/null; then
73
+ echo "\$pkg" >> "$BEFORE_FILE"
74
+ fi
75
+ done
76
+
77
+ if [ ! -f /usr/share/keyrings/docker-archive-keyring.gpg ]; then
78
+ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
79
+ fi
80
+
81
+ if [ ! -f /etc/apt/sources.list.d/docker.list ]; then
82
+ echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
83
+ fi
84
+
85
+ echo "✍🏻 Installing Docker Engine..."
86
+ sudo apt-get update -y
87
+ sudo apt-get install -y "\${TARGET_PKGS[@]}"
88
+ sudo touch "$AFTER_FILE" && echo " - marker at $AFTER_FILE"
89
+ echo "✅ Docker installed"
90
+ else
91
+ echo "🔄 Docker already set up, ensuring latest..."
92
+ sudo apt-get update -y
93
+ sudo apt-get install -y "\${TARGET_PKGS[@]}"
94
+ echo "✅ Docker packages are current"
95
+ fi
96
+ EOH
97
+ say output
98
+ say "✅ Docker Engine setup completed", :green
99
+ end
100
+
101
+ def perform_rollback(ssh)
102
+ output = ssh.exec! <<~EOH
103
+ set -e
104
+
105
+ sudo mkdir -p /usr/local/backups
106
+ sudo chown deploy:deploy /usr/local/backups
107
+
108
+ BACKUP_DIR="/usr/local/backups"
109
+ SCRIPT_ID="install_docker_engine"
110
+ BEFORE_FILE="${BACKUP_DIR}/${SCRIPT_ID}.before"
111
+ AFTER_FILE="${BACKUP_DIR}/${SCRIPT_ID}.after"
112
+
113
+ TARGET_PKGS=(docker-ce docker-ce-cli containerd.io)
114
+
115
+ echo "✍🏻 TARGET_PKGS=(\${TARGET_PKGS[*]})"
116
+
117
+ if [ -f "$AFTER_FILE" ]; then
118
+ to_remove=()
119
+ for pkg in "\${TARGET_PKGS[@]}"; do
120
+ if dpkg -l "\$pkg" &>/dev/null && ! grep -Fxq "\$pkg" "$BEFORE_FILE"; then
121
+ to_remove+=("\$pkg")
122
+ fi
123
+ done
124
+
125
+ if [ \${#to_remove[@]} -gt 0 ]; then
126
+ echo "🔁 Removing Docker packages..."
127
+ sudo apt-get remove -y "\${to_remove[@]}" && echo " - removed: \${to_remove[*]}"
128
+ else
129
+ echo " - no Docker packages to remove"
130
+ fi
131
+
132
+ sudo rm -f "$BEFORE_FILE" "$AFTER_FILE" && echo " - cleanup markers"
133
+ else
134
+ echo " - no marker for $SCRIPT_ID, skipping"
135
+ fi
136
+
137
+ echo "✅ Rollback done"
138
+ EOH
139
+ say output
140
+ say "✅ Docker Engine rollback completed", :green
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,142 @@
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 PostinstallDocker < Thor
10
+ namespace "postinstall_docker"
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", "Apply Docker post-install configuration (start service, add groups, create network)"
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(filled_options) do |ssh|
25
+ perform_setup(ssh)
26
+ end
27
+ end
28
+
29
+ desc "rollback", "Undo Docker post-install configuration"
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
+ with_ssh_connection(filled_options) do |ssh|
38
+ perform_rollback(ssh)
39
+ end
40
+ end
41
+
42
+ no_commands do
43
+ def with_ssh_connection(filled_options)
44
+ server = filled_options[:server_ip]
45
+ port = filled_options[:ssh_port]
46
+ key = File.expand_path(filled_options[:ssh_key_path])
47
+
48
+ say "🔑 Connecting as deploy@#{server}:#{port}", :green
49
+ Net::SSH.start(server, "deploy", port: port, keys: [key], non_interactive: true, timeout: 5) do |ssh|
50
+ yield ssh
51
+ end
52
+ end
53
+
54
+ def perform_setup(ssh)
55
+ output = ssh.exec! <<~EOH
56
+ set -e
57
+
58
+ sudo mkdir -p /usr/local/backups
59
+ sudo chown deploy:deploy /usr/local/backups
60
+
61
+ BACKUP_DIR="/usr/local/backups"
62
+ SCRIPT_ID="postinstall_docker"
63
+ BEFORE_FILE="${BACKUP_DIR}/${SCRIPT_ID}.before"
64
+ AFTER_FILE="${BACKUP_DIR}/${SCRIPT_ID}.after"
65
+
66
+ echo "✍🏻 Performing post-install Docker tasks"
67
+
68
+ if [ ! -f "$AFTER_FILE" ]; then
69
+ # Record state
70
+ systemctl is-enabled docker &>/dev/null && echo "docker.service enabled" >> "$BEFORE_FILE" || echo "docker.service disabled" >> "$BEFORE_FILE"
71
+ groups deploy | grep -q docker && echo "deploy in docker group" >> "$BEFORE_FILE" || echo "deploy not in docker group" >> "$BEFORE_FILE"
72
+ sudo docker network inspect private &>/dev/null && echo "network private exists" >> "$BEFORE_FILE" || echo "network private absent" >> "$BEFORE_FILE"
73
+
74
+ # Start and enable Docker
75
+ sudo systemctl start docker
76
+ sudo systemctl enable docker
77
+ echo "🚀 Docker service started and enabled"
78
+ echo "docker.service enabled" >> "$AFTER_FILE"
79
+
80
+ # Add deploy to docker group
81
+ sudo usermod -aG docker deploy
82
+ echo "👥 Added 'deploy' to docker group"
83
+ echo "added docker group" >> "$AFTER_FILE"
84
+
85
+ # Create private network if missing
86
+ if ! sudo docker network inspect private &>/dev/null; then
87
+ sudo docker network create -d bridge private
88
+ echo "🌐 Created Docker network 'private'"
89
+ echo "created network private" >> "$AFTER_FILE"
90
+ fi
91
+
92
+ echo "✅ Post-install Docker tasks complete"
93
+ else
94
+ echo "🔄 Post-install tasks already applied, skipping setup"
95
+ fi
96
+ EOH
97
+ say output
98
+ say "✅ Post-install Docker setup completed", :green
99
+ end
100
+
101
+ def perform_rollback(ssh)
102
+ output = ssh.exec! <<~EOH
103
+ set -e
104
+
105
+ sudo mkdir -p /usr/local/backups
106
+ sudo chown deploy:deploy /usr/local/backups
107
+
108
+ BACKUP_DIR="/usr/local/backups"
109
+ SCRIPT_ID="postinstall_docker"
110
+ BEFORE_FILE="${BACKUP_DIR}/${SCRIPT_ID}.before"
111
+ AFTER_FILE="${BACKUP_DIR}/${SCRIPT_ID}.after"
112
+
113
+ echo "🔄 Rolling back post-install Docker tasks..."
114
+
115
+ if [ -f "$AFTER_FILE" ]; then
116
+ if grep -Fxq "docker.service enabled" "$AFTER_FILE"; then
117
+ sudo systemctl disable docker
118
+ echo " - Docker service disabled"
119
+ fi
120
+ if grep -Fxq "added docker group" "$AFTER_FILE"; then
121
+ sudo gpasswd -d deploy docker || true
122
+ echo " - Removed 'deploy' from docker group"
123
+ fi
124
+ if grep -Fxq "created network private" "$AFTER_FILE"; then
125
+ sudo docker network rm private || true
126
+ echo " - Removed Docker network 'private'"
127
+ fi
128
+ sudo rm -f "$BEFORE_FILE" "$AFTER_FILE"
129
+ else
130
+ echo " - no marker for $SCRIPT_ID, skipping rollback"
131
+ fi
132
+
133
+ echo "✅ Rollback complete"
134
+ EOH
135
+ say output
136
+ say "✅ Post-install Docker rollback completed", :green
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,43 @@
1
+ require "thor"
2
+ require_relative "../defaults"
3
+ require_relative "../provisioner"
4
+ require_relative "../options_builder"
5
+
6
+ module Kitsune
7
+ module Kit
8
+ module Commands
9
+ class Provision < Thor
10
+ namespace "provision"
11
+
12
+ class_option :droplet_name, type: :string, aliases: "-n", desc: "Droplet name"
13
+ class_option :region, type: :string, aliases: "-r", desc: "Region"
14
+ class_option :size, type: :string, aliases: "-s", desc: "Size"
15
+ class_option :image, type: :string, aliases: "-i", desc: "Image"
16
+ class_option :tag, type: :string, aliases: "-t", desc: "Tag to filter/create"
17
+ class_option :ssh_key_id, type: :string, aliases: "-k", desc: "SSH key ID"
18
+
19
+ desc "create", "Create the Droplet if it doesn't exist"
20
+ def create
21
+ filled_options = Kitsune::Kit::OptionsBuilder.build(
22
+ options,
23
+ required: [:ssh_key_id],
24
+ defaults: Kitsune::Kit::Defaults.infra
25
+ )
26
+
27
+ Provisioner.new(filled_options).create_or_show
28
+ end
29
+
30
+ desc "rollback", "Remove the Droplet if it exists"
31
+ def rollback
32
+ filled_options = Kitsune::Kit::OptionsBuilder.build(
33
+ options,
34
+ required: [:ssh_key_id],
35
+ defaults: Kitsune::Kit::Defaults.infra
36
+ )
37
+
38
+ Provisioner.new(filled_options).rollback
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,150 @@
1
+ require "thor"
2
+ require "net/ssh"
3
+ require_relative "../options_builder"
4
+ require_relative "../defaults"
5
+
6
+ module Kitsune
7
+ module Kit
8
+ module Commands
9
+ class SetupDockerPrereqs < Thor
10
+ namespace "setup_docker_prereqs"
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", "Install Docker prerequisites on the 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(filled_options) do |ssh|
25
+ perform_setup(ssh)
26
+ end
27
+ end
28
+
29
+ desc "rollback", "Remove installed Docker prerequisites from the 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
+ with_ssh_connection(filled_options) do |ssh|
38
+ perform_rollback(ssh)
39
+ end
40
+ end
41
+
42
+ no_commands do
43
+ def with_ssh_connection(filled_options)
44
+ server = filled_options[:server_ip]
45
+ port = filled_options[:ssh_port]
46
+ key = File.expand_path(filled_options[:ssh_key_path])
47
+
48
+ say "🔑 Connecting as deploy@#{server}:#{port}", :green
49
+ Net::SSH.start(server, "deploy", port: port, keys: [key], non_interactive: true, timeout: 5) do |ssh|
50
+ yield ssh
51
+ end
52
+ end
53
+
54
+ def perform_setup(ssh)
55
+ output = ssh.exec! <<~EOH
56
+ set -e
57
+
58
+ sudo mkdir -p /usr/local/backups
59
+ sudo chown deploy:deploy /usr/local/backups
60
+
61
+ BACKUP_DIR="/usr/local/backups"
62
+ SCRIPT_ID="setup_docker_prereqs"
63
+ BEFORE_FILE="${BACKUP_DIR}/${SCRIPT_ID}.before"
64
+ AFTER_FILE="${BACKUP_DIR}/${SCRIPT_ID}.after"
65
+
66
+ TARGET_PKGS=(
67
+ apt-transport-https
68
+ ca-certificates
69
+ curl
70
+ gnupg
71
+ lsb-release
72
+ software-properties-common
73
+ )
74
+
75
+ echo "✍🏻 TARGET_PKGS=(\${TARGET_PKGS[*]})"
76
+
77
+ if [ ! -f "$AFTER_FILE" ]; then
78
+ for pkg in "\${TARGET_PKGS[@]}"; do
79
+ if dpkg -l "\$pkg" &>/dev/null; then
80
+ echo "\$pkg" >> "$BEFORE_FILE"
81
+ fi
82
+ done
83
+
84
+ echo "✍🏻 Installing prerequisites..."
85
+ sudo apt-get update -y
86
+ sudo apt-get install -y "\${TARGET_PKGS[@]}"
87
+ sudo touch "$AFTER_FILE" && echo " - marker created at $AFTER_FILE"
88
+ else
89
+ sudo apt-get update -y
90
+ sudo apt-get install -y "\${TARGET_PKGS[@]}"
91
+ echo "✅ Prerequisites are current"
92
+ fi
93
+ EOH
94
+ say output
95
+ say "✅ Docker prerequisites setup completed", :green
96
+ end
97
+
98
+ def perform_rollback(ssh)
99
+ output = ssh.exec! <<~EOH
100
+ set -e
101
+
102
+ sudo mkdir -p /usr/local/backups
103
+ sudo chown deploy:deploy /usr/local/backups
104
+
105
+ BACKUP_DIR="/usr/local/backups"
106
+ SCRIPT_ID="setup_docker_prereqs"
107
+ BEFORE_FILE="${BACKUP_DIR}/${SCRIPT_ID}.before"
108
+ AFTER_FILE="${BACKUP_DIR}/${SCRIPT_ID}.after"
109
+
110
+ TARGET_PKGS=(
111
+ apt-transport-https
112
+ ca-certificates
113
+ curl
114
+ gnupg
115
+ lsb-release
116
+ software-properties-common
117
+ )
118
+
119
+ echo "✍🏻 TARGET_PKGS=(\${TARGET_PKGS[*]})"
120
+
121
+ if [ -f "$AFTER_FILE" ]; then
122
+ to_remove=()
123
+ for pkg in "\${TARGET_PKGS[@]}"; do
124
+ if dpkg -l "\$pkg" &>/dev/null; then
125
+ if ! grep -Fxq "\$pkg" "$BEFORE_FILE"; then
126
+ to_remove+=("\$pkg")
127
+ fi
128
+ fi
129
+ done
130
+
131
+ if [ \${#to_remove[@]} -gt 0 ]; then
132
+ echo "🔁 Removing packages..."
133
+ sudo apt-get remove -y "\${to_remove[@]}" && echo " - removed: \${to_remove[*]}"
134
+ else
135
+ echo " - no new packages to remove"
136
+ fi
137
+
138
+ sudo rm -f "$BEFORE_FILE" "$AFTER_FILE" && echo " - backups removed"
139
+ else
140
+ echo " - no marker for $SCRIPT_ID, skipping removal"
141
+ fi
142
+ EOH
143
+ say output
144
+ say "✅ Docker prerequisites rollback completed", :green
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end