castled 0.1.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b32ade60008579e7327153766c0cb2863380e65676f74333ddefdfaf576d496a
4
+ data.tar.gz: d130e839828dd6b38a40317698f4f6eda5f974b68a99d009792f70a581201bdc
5
+ SHA512:
6
+ metadata.gz: bef65346414a4945081b6f11fd961576b42a890a184be7cb35d3a1dd992c852f3ae92e4e3792933888366c889efa63fe3f6303310dbcbb8edc9a9b3b09fb2580
7
+ data.tar.gz: dc0d75e763b14b13bd3f7838cb5bb713fc71dd89179a7b95b6b7f7bfe51ea27c63faeb37ca8442dcc8f16572beea3ea5c3882afa3f4d37c8e14395eb3ec8795f
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
6
+
7
+ group :development do
8
+ gem "rake"
9
+ end
10
+
11
+ gem "minitest", "~> 6.0", group: :development
data/Gemfile.lock ADDED
@@ -0,0 +1,33 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ castled (0.1.1)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ drb (2.2.3)
10
+ minitest (6.0.6)
11
+ drb (~> 2.0)
12
+ prism (~> 1.5)
13
+ prism (1.9.0)
14
+ rake (13.4.2)
15
+
16
+ PLATFORMS
17
+ ruby
18
+ x86_64-linux
19
+
20
+ DEPENDENCIES
21
+ castled!
22
+ minitest (~> 6.0)
23
+ rake (>= 13.0, >= 0)
24
+
25
+ CHECKSUMS
26
+ castled (0.1.1)
27
+ drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373
28
+ minitest (6.0.6) sha256=153ea36d1d987a62942382b61075745042a2b3123b1cd48f4c3675af9cc7d6f1
29
+ prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
30
+ rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
31
+
32
+ BUNDLED WITH
33
+ 4.0.6
data/README.md ADDED
@@ -0,0 +1,109 @@
1
+ # Castled
2
+
3
+ **Simple backups for Omarchy. Opinionated defaults, zero ceremony.**
4
+
5
+ Omarchy gives you a beautiful, modern, fully configured Linux system in one shot — the omakase menu, chef's choice. Castled applies the same spirit to backups: a tiny, plain-text tool that saves the dotfiles you care about without turning disaster recovery into another configuration hobby.
6
+
7
+ No bespoke backup framework. No paradox of choice. Just `init`, `backup`, and `restore`.
8
+
9
+ ## Why Castled?
10
+
11
+ - **Curated from the start** — `simple-backup init` writes a sensible Omarchy-oriented `config.yml` you can edit in seconds.
12
+ - **Plain text, terminal-first** — one YAML file lists what to save and where; everything else stays out of your way.
13
+ - **Restore when it matters** — list backups, pick one, preview with `--dry-run`, diff with `--diff`.
14
+ - **Substitutions welcome** — change paths, add folders, point `destination` at your USB drive. The defaults are a starting point, not a contract.
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ gem build castled.gemspec
20
+ gem install castled-*.gem
21
+ ```
22
+
23
+ Or from the project directory:
24
+
25
+ ```bash
26
+ bundle install
27
+ bundle exec rake install
28
+ ```
29
+
30
+ ## Quick start
31
+
32
+ ### Initialize
33
+
34
+ Create a `config.yml` in the current directory:
35
+
36
+ ```bash
37
+ simple-backup init
38
+ ```
39
+
40
+ Example `config.yml` (also what `init` generates):
41
+
42
+ ```yaml
43
+ backup_name: omarchy
44
+ backup_paths:
45
+ - ~/.bash_logout
46
+ - ~/.bash_profile
47
+ - ~/.bashrc
48
+ - ~/.gemrc
49
+ - ~/.XCompose
50
+ destination: /run/media/leonid/250GB/backups
51
+ ```
52
+
53
+ Adjust `backup_paths` and `destination` to match your machine. Plug in an external drive, point `destination` at its mount path, and you're done.
54
+
55
+ ### Backup
56
+
57
+ Copy configured paths to a timestamped folder under `destination`:
58
+
59
+ ```bash
60
+ simple-backup backup
61
+ ```
62
+
63
+ Backups are stored as `backup_name_YYYYMMDD_HHMMSS` (e.g. `omarchy_20260519_112300`).
64
+
65
+ ### Schedule with cron
66
+
67
+ Run backups automatically from the directory that holds your `config.yml`:
68
+
69
+ ```bash
70
+ crontab -e
71
+ ```
72
+
73
+ Example — backup every day at 9:00 AM:
74
+
75
+ ```cron
76
+ 0 9 * * * cd /path/to/backup-config && /usr/bin/env simple-backup backup
77
+ ```
78
+
79
+ Use full paths in cron jobs when possible; cron runs with a smaller environment than your interactive shell.
80
+
81
+ ### Restore
82
+
83
+ List available backups and restore one interactively:
84
+
85
+ ```bash
86
+ simple-backup restore
87
+ ```
88
+
89
+ Restore copies files back to their original locations (overwriting existing files).
90
+
91
+ Preview without writing:
92
+
93
+ ```bash
94
+ simple-backup restore --dry-run
95
+ simple-backup restore 1 --dry-run --diff
96
+ ```
97
+
98
+ Dry run reports:
99
+
100
+ - files that would be copied to new paths
101
+ - files that would overwrite existing paths
102
+ - optional unified diffs for overwritten text files (`--diff`, requires `--dry-run`)
103
+
104
+ ## Development
105
+
106
+ ```bash
107
+ bundle install
108
+ bundle exec ruby -Ilib:test test/test_*.rb
109
+ ```
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rake/testtask"
4
+
5
+ Rake::TestTask.new(:test) do |t|
6
+ t.libs << "test"
7
+ t.libs << "lib"
8
+ t.test_files = FileList["test/**/test_*.rb"]
9
+ t.verbose = true
10
+ end
11
+
12
+ task default: :test
data/castled.gemspec ADDED
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/castled/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "castled"
7
+ spec.version = Castled::VERSION
8
+ spec.authors = ["Castled"]
9
+ spec.email = ["castled@example.com"]
10
+
11
+ spec.summary = "Simple backups via simple-backup CLI"
12
+ spec.description = "Initialize, backup, and restore files with a YAML config"
13
+ spec.homepage = "https://github.com/example/castled"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 3.0.0"
16
+
17
+ spec.files = Dir.chdir(__dir__) do
18
+ `git ls-files -z`.split("\x0").reject do |f|
19
+ f.start_with?("test/", ".git")
20
+ end
21
+ rescue StandardError
22
+ %w[
23
+ castled.gemspec
24
+ Gemfile
25
+ README.md
26
+ exe/simple-backup
27
+ lib/castled.rb
28
+ lib/castled/version.rb
29
+ lib/castled/cli.rb
30
+ lib/castled/config.rb
31
+ lib/castled/backup.rb
32
+ test/test_helper.rb
33
+ test/test_config.rb
34
+ test/test_backup.rb
35
+ test/test_cli.rb
36
+ ]
37
+ end
38
+
39
+ spec.bindir = "exe"
40
+ spec.executables = ["simple-backup"]
41
+ spec.require_paths = ["lib"]
42
+
43
+ spec.add_development_dependency "rake", ">= 13.0"
44
+ end
data/docs/index.html ADDED
@@ -0,0 +1,152 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Castled — Simple backups for Omarchy</title>
7
+ <meta name="description" content="A tiny, opinionated backup tool for Omarchy users. Initialize once, back up your dotfiles, restore when you need them.">
8
+ <style>
9
+ :root {
10
+ --bg: #0f1114;
11
+ --surface: #1a1d23;
12
+ --border: #2a3038;
13
+ --text: #e8eaed;
14
+ --muted: #9aa0a6;
15
+ --accent: #7eb8da;
16
+ --accent-dim: #5a8fad;
17
+ }
18
+ * { box-sizing: border-box; margin: 0; padding: 0; }
19
+ body {
20
+ font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
21
+ background: var(--bg);
22
+ color: var(--text);
23
+ line-height: 1.6;
24
+ min-height: 100vh;
25
+ }
26
+ .wrap {
27
+ max-width: 42rem;
28
+ margin: 0 auto;
29
+ padding: 3rem 1.5rem 4rem;
30
+ }
31
+ h1 {
32
+ font-size: 1.75rem;
33
+ font-weight: 600;
34
+ letter-spacing: -0.02em;
35
+ margin-bottom: 0.5rem;
36
+ }
37
+ .tagline {
38
+ font-size: 1.125rem;
39
+ color: var(--muted);
40
+ margin-bottom: 2rem;
41
+ }
42
+ .pitch {
43
+ margin-bottom: 2.5rem;
44
+ color: var(--muted);
45
+ }
46
+ .pitch strong { color: var(--text); font-weight: 500; }
47
+ h2 {
48
+ font-size: 0.75rem;
49
+ font-weight: 600;
50
+ text-transform: uppercase;
51
+ letter-spacing: 0.08em;
52
+ color: var(--accent-dim);
53
+ margin: 2rem 0 0.75rem;
54
+ }
55
+ pre, code {
56
+ font-family: ui-monospace, "Cascadia Code", "SF Mono", Menlo, Consolas, monospace;
57
+ font-size: 0.8125rem;
58
+ }
59
+ pre {
60
+ background: var(--surface);
61
+ border: 1px solid var(--border);
62
+ border-radius: 6px;
63
+ padding: 1rem 1.25rem;
64
+ overflow-x: auto;
65
+ margin-bottom: 0.5rem;
66
+ }
67
+ .cmd { color: var(--accent); }
68
+ .comment { color: var(--muted); }
69
+ ul.features {
70
+ list-style: none;
71
+ margin: 1.5rem 0;
72
+ }
73
+ ul.features li {
74
+ padding: 0.35rem 0;
75
+ padding-left: 1.25rem;
76
+ position: relative;
77
+ color: var(--muted);
78
+ }
79
+ ul.features li::before {
80
+ content: "→";
81
+ position: absolute;
82
+ left: 0;
83
+ color: var(--accent-dim);
84
+ }
85
+ footer {
86
+ margin-top: 3rem;
87
+ padding-top: 1.5rem;
88
+ border-top: 1px solid var(--border);
89
+ font-size: 0.875rem;
90
+ color: var(--muted);
91
+ }
92
+ footer a { color: var(--accent); text-decoration: none; }
93
+ footer a:hover { text-decoration: underline; }
94
+ </style>
95
+ </head>
96
+ <body>
97
+ <div class="wrap">
98
+ <h1>Castled</h1>
99
+ <p class="tagline">Simple backups for Omarchy. Opinionated defaults, zero ceremony.</p>
100
+
101
+ <p class="pitch">
102
+ Omarchy is <strong>beautiful, modern &amp; opinionated Linux</strong> — omakase computing,
103
+ chef's choice. Castled brings the same spirit to your personal dotfiles: one command to
104
+ initialize, one to back up, one to restore. No backup framework to learn. Substitutions
105
+ welcome.
106
+ </p>
107
+
108
+ <ul class="features">
109
+ <li>Curated <code>config.yml</code> from <code>simple-backup init</code></li>
110
+ <li>Timestamped copies to any destination (USB, NAS, local disk)</li>
111
+ <li>Interactive restore with dry-run and diff preview</li>
112
+ </ul>
113
+
114
+ <h2>Install</h2>
115
+ <pre><code><span class="cmd">gem build castled.gemspec</span>
116
+ <span class="cmd">gem install castled-*.gem</span></code></pre>
117
+
118
+ <h2>Initialize</h2>
119
+ <pre><code><span class="cmd">simple-backup init</span>
120
+ <span class="comment"># creates config.yml with Omarchy-friendly defaults</span></code></pre>
121
+
122
+ <h2>Example config</h2>
123
+ <pre><code>backup_name: omarchy
124
+ backup_paths:
125
+ - ~/.bash_logout
126
+ - ~/.bash_profile
127
+ - ~/.bashrc
128
+ - ~/.gemrc
129
+ - ~/.XCompose
130
+ destination: /run/media/leonid/250GB/backups</code></pre>
131
+
132
+ <h2>Backup &amp; restore</h2>
133
+ <pre><code><span class="cmd">simple-backup backup</span>
134
+ <span class="comment"># → omarchy_YYYYMMDD_HHMMSS/</span>
135
+
136
+ <span class="cmd">simple-backup restore</span>
137
+ <span class="cmd">simple-backup restore --dry-run --diff</span></code></pre>
138
+
139
+ <h2>Schedule (optional)</h2>
140
+ <pre><code><span class="comment"># crontab — daily at 9:00</span>
141
+ 0 9 * * * cd /path/to/config &amp;&amp; simple-backup backup</code></pre>
142
+
143
+ <footer>
144
+ <a href="https://github.com/example/castled">GitHub</a>
145
+ ·
146
+ <a href="https://omarchy.org">Omarchy</a>
147
+ ·
148
+ MIT License
149
+ </footer>
150
+ </div>
151
+ </body>
152
+ </html>
data/exe/simple-backup ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
5
+ require "castled"
6
+
7
+ Castled::CLI.run(ARGV)
@@ -0,0 +1,216 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "pathname"
5
+ require "time"
6
+ require "yaml"
7
+
8
+ module Castled
9
+ class Backup
10
+ MANIFEST_FILENAME = ".castled_manifest.yml"
11
+
12
+ class Error < StandardError; end
13
+
14
+ def initialize(config)
15
+ @config = config
16
+ end
17
+
18
+ def run!
19
+ timestamp = Time.now.strftime("%Y%m%d_%H%M%S")
20
+ backup_dir = @config.destination_path.join("#{@config.backup_name}_#{timestamp}")
21
+ FileUtils.mkdir_p(backup_dir)
22
+
23
+ entries = []
24
+ @config.backup_paths.each do |source_path|
25
+ source = Pathname.new(source_path).expand_path
26
+ raise Error, "Path not found: #{source}" unless source.exist?
27
+
28
+ dest_name = source.basename.to_s
29
+ dest = backup_dir.join(dest_name)
30
+ copy_entry(source, dest)
31
+ entries << { "source" => source.to_s, "backup_entry" => dest_name }
32
+ end
33
+
34
+ write_manifest(backup_dir, entries)
35
+ backup_dir.to_s
36
+ end
37
+
38
+ def restore!(selection: nil, dry_run: false, diff: false, io: $stdout)
39
+ backups = list_backups
40
+ raise Error, "No backups found for '#{@config.backup_name}'" if backups.empty?
41
+
42
+ chosen = select_backup(backups, selection: selection)
43
+ manifest = load_manifest(chosen)
44
+ plan = restore_plan(chosen, manifest)
45
+
46
+ if dry_run
47
+ print_dry_run(chosen, plan, diff: diff, io: io)
48
+ return chosen.to_s
49
+ end
50
+
51
+ plan.each do |operation|
52
+ restore_operation(operation)
53
+ end
54
+
55
+ chosen.to_s
56
+ end
57
+
58
+ def list_backups
59
+ dest = @config.destination_path
60
+ return [] unless dest.directory?
61
+
62
+ prefix = "#{@config.backup_name}_"
63
+ dest.children
64
+ .select(&:directory?)
65
+ .select { |d| d.basename.to_s.start_with?(prefix) }
66
+ .sort_by { |d| d.basename.to_s }
67
+ .reverse
68
+ end
69
+
70
+ private
71
+
72
+ def restore_plan(backup_dir, manifest)
73
+ manifest.fetch("entries").flat_map do |entry|
74
+ source = Pathname.new(entry.fetch("source"))
75
+ backup_entry = backup_dir.join(entry.fetch("backup_entry"))
76
+ raise Error, "Backup entry missing: #{backup_entry}" unless backup_entry.exist?
77
+
78
+ if backup_entry.directory?
79
+ directory_restore_plan(backup_entry, source)
80
+ else
81
+ [file_restore_operation(backup_entry, source)]
82
+ end
83
+ end
84
+ end
85
+
86
+ def directory_restore_plan(backup_entry, source)
87
+ operations = []
88
+ operations << { action: :mkdir, backup: backup_entry, target: source } unless source.directory?
89
+
90
+ backup_entry.find.each do |path|
91
+ next if path == backup_entry
92
+
93
+ relative_path = path.relative_path_from(backup_entry)
94
+ target = source.join(relative_path)
95
+
96
+ if path.directory?
97
+ operations << { action: :mkdir, backup: path, target: target } unless target.directory?
98
+ else
99
+ operations << file_restore_operation(path, target)
100
+ end
101
+ end
102
+
103
+ operations
104
+ end
105
+
106
+ def file_restore_operation(backup_entry, target)
107
+ action = target.exist? ? :overwrite : :copy
108
+ { action: action, backup: backup_entry, target: target }
109
+ end
110
+
111
+ def restore_operation(operation)
112
+ case operation[:action]
113
+ when :mkdir
114
+ FileUtils.mkdir_p(operation[:target])
115
+ when :copy, :overwrite
116
+ FileUtils.mkdir_p(operation[:target].dirname)
117
+ FileUtils.cp(operation[:backup].to_s, operation[:target].to_s)
118
+ end
119
+ end
120
+
121
+ def print_dry_run(backup_dir, plan, diff:, io:)
122
+ io.puts "Dry run: restoring #{backup_dir.basename}"
123
+
124
+ if plan.empty?
125
+ io.puts "No files would be copied."
126
+ return
127
+ end
128
+
129
+ plan.each do |operation|
130
+ case operation[:action]
131
+ when :mkdir
132
+ io.puts "Would create directory: #{operation[:target]}"
133
+ when :copy
134
+ io.puts "Would copy: #{operation[:backup]} -> #{operation[:target]}"
135
+ when :overwrite
136
+ io.puts "Would overwrite: #{operation[:target]} with #{operation[:backup]}"
137
+ print_diff(operation, io: io) if diff
138
+ end
139
+ end
140
+ end
141
+
142
+ def print_diff(operation, io:)
143
+ current = operation[:target].read
144
+ restored = operation[:backup].read
145
+ return if current == restored
146
+
147
+ io.puts "Diff for #{operation[:target]}:"
148
+ io.puts "--- current"
149
+ io.puts "+++ restored"
150
+ io.puts unified_line_diff(current, restored)
151
+ rescue ArgumentError
152
+ io.puts "Diff skipped for #{operation[:target]}: file is not valid text"
153
+ end
154
+
155
+ def unified_line_diff(current, restored)
156
+ current_lines = current.lines
157
+ restored_lines = restored.lines
158
+ max = [current_lines.length, restored_lines.length].max
159
+ output = []
160
+
161
+ max.times do |index|
162
+ old_line = current_lines[index]
163
+ new_line = restored_lines[index]
164
+ next if old_line == new_line
165
+
166
+ output << "-#{old_line}" if old_line
167
+ output << "+#{new_line}" if new_line
168
+ end
169
+
170
+ output.join
171
+ end
172
+
173
+ def copy_entry(source, dest)
174
+ if source.directory?
175
+ FileUtils.mkdir_p(dest)
176
+ source.children.each do |child|
177
+ FileUtils.cp_r(child.to_s, dest.to_s)
178
+ end
179
+ else
180
+ FileUtils.mkdir_p(dest.dirname)
181
+ FileUtils.cp(source.to_s, dest.to_s)
182
+ end
183
+ end
184
+
185
+ def write_manifest(backup_dir, entries)
186
+ manifest = {
187
+ "backup_name" => @config.backup_name,
188
+ "created_at" => Time.now.iso8601,
189
+ "entries" => entries
190
+ }
191
+ backup_dir.join(MANIFEST_FILENAME).write(YAML.dump(manifest))
192
+ end
193
+
194
+ def load_manifest(backup_dir)
195
+ path = backup_dir.join(MANIFEST_FILENAME)
196
+ raise Error, "Manifest not found in #{backup_dir}" unless path.exist?
197
+
198
+ YAML.safe_load(path.read) || {}
199
+ end
200
+
201
+ def select_backup(backups, selection: nil)
202
+ return backups[selection.to_i - 1] if selection
203
+
204
+ puts "Available backups:"
205
+ backups.each_with_index do |backup, index|
206
+ puts " #{index + 1}. #{backup.basename}"
207
+ end
208
+ print "Select backup (1-#{backups.size}): "
209
+ choice = $stdin.gets&.strip
210
+ index = choice.to_i
211
+ raise Error, "Invalid selection" if index < 1 || index > backups.size
212
+
213
+ backups[index - 1]
214
+ end
215
+ end
216
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castled
4
+ class CLI
5
+ COMMANDS = %w[init backup restore].freeze
6
+
7
+ def self.run(argv)
8
+ new.run(argv)
9
+ end
10
+
11
+ def run(argv)
12
+ command = argv.first
13
+ if command.nil? || command == "--help" || command == "-h"
14
+ print_usage
15
+ exit command.nil? ? 1 : 0
16
+ end
17
+
18
+ unless COMMANDS.include?(command)
19
+ $stderr.puts "Unknown command: #{command}"
20
+ print_usage
21
+ exit 1
22
+ end
23
+
24
+ send(command, argv[1..])
25
+ rescue Config::Error, Backup::Error => e
26
+ $stderr.puts "Error: #{e.message}"
27
+ exit 1
28
+ end
29
+
30
+ private
31
+
32
+ def init(_args)
33
+ path = Config.init!
34
+ puts "Created #{path}"
35
+ end
36
+
37
+ def backup(_args)
38
+ config = Config.load!
39
+ backup_dir = Backup.new(config).run!
40
+ puts "Backup created at #{backup_dir}"
41
+ end
42
+
43
+ def restore(args)
44
+ options = parse_restore_options(args)
45
+ config = Config.load!
46
+ restored = Backup.new(config).restore!(
47
+ selection: options[:selection],
48
+ dry_run: options[:dry_run],
49
+ diff: options[:diff]
50
+ )
51
+
52
+ if options[:dry_run]
53
+ puts "Dry run complete for #{restored}"
54
+ else
55
+ puts "Restored from #{restored}"
56
+ end
57
+ end
58
+
59
+ def parse_restore_options(args)
60
+ dry_run = false
61
+ diff = false
62
+ selection = nil
63
+ unknown = []
64
+
65
+ args.each do |arg|
66
+ case arg
67
+ when "--dry-run", "-n"
68
+ dry_run = true
69
+ when "--diff"
70
+ diff = true
71
+ when /\A\d+\z/
72
+ selection ||= arg
73
+ else
74
+ unknown << arg
75
+ end
76
+ end
77
+
78
+ raise Backup::Error, "Unknown restore options: #{unknown.join(', ')}" unless unknown.empty?
79
+ raise Backup::Error, "--diff requires --dry-run" if diff && !dry_run
80
+
81
+ { dry_run: dry_run, diff: diff, selection: selection }
82
+ end
83
+
84
+ def print_usage
85
+ puts <<~USAGE
86
+ Usage: simple-backup <command>
87
+
88
+ Commands:
89
+ init Create config.yml in the current directory
90
+ backup Copy configured paths to a timestamped backup folder
91
+ restore List backups and restore one interactively
92
+
93
+ Restore options:
94
+ --dry-run, -n Preview files that would be copied or overwritten
95
+ --diff Show unified diffs for files that would be overwritten
96
+ USAGE
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "pathname"
5
+
6
+ module Castled
7
+ class Config
8
+ CONFIG_FILENAME = "config.yml"
9
+
10
+ DEFAULT_TEMPLATE = <<~YAML
11
+ backup_name: omarchy
12
+ backup_paths:
13
+ - ~/.bash_logout
14
+ - ~/.bash_profile
15
+ - ~/.bashrc
16
+ - ~/.gemrc
17
+ - ~/.XCompose
18
+ destination: /run/media/leonid/250GB/backups
19
+ YAML
20
+
21
+ class Error < StandardError; end
22
+
23
+ attr_reader :backup_name, :backup_paths, :destination
24
+
25
+ def self.init!(dir: Dir.pwd)
26
+ path = Pathname.new(dir).join(CONFIG_FILENAME)
27
+ raise Error, "#{CONFIG_FILENAME} already exists" if path.exist?
28
+
29
+ path.write(DEFAULT_TEMPLATE)
30
+ path.to_s
31
+ end
32
+
33
+ def self.load!(dir: Dir.pwd)
34
+ path = Pathname.new(dir).join(CONFIG_FILENAME)
35
+ raise Error, "#{CONFIG_FILENAME} not found. Run `simple-backup init` first." unless path.exist?
36
+
37
+ new(path)
38
+ end
39
+
40
+ def initialize(path)
41
+ @path = Pathname.new(path)
42
+ data = YAML.safe_load(@path.read, permitted_classes: [Symbol], aliases: true) || {}
43
+ @backup_name = data["backup_name"] || data[:backup_name]
44
+ @backup_paths = Array(data["backup_paths"] || data[:backup_paths])
45
+ @destination = data["destination"] || data[:destination]
46
+ validate!
47
+ end
48
+
49
+ def destination_path
50
+ Pathname.new(@destination).expand_path
51
+ end
52
+
53
+ private
54
+
55
+ def validate!
56
+ raise Error, "backup_name is required" if @backup_name.nil? || @backup_name.to_s.strip.empty?
57
+
58
+ if @backup_paths.empty?
59
+ raise Error, "backup_paths must list at least one file or folder"
60
+ end
61
+
62
+ if @destination.nil? || @destination.to_s.strip.empty?
63
+ raise Error, "destination is required"
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castled
4
+ VERSION = "0.1.1"
5
+ end
data/lib/castled.rb ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "castled/version"
4
+ require_relative "castled/config"
5
+ require_relative "castled/backup"
6
+ require_relative "castled/cli"
7
+
8
+ module Castled
9
+ end
metadata ADDED
@@ -0,0 +1,67 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: castled
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Castled
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rake
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '13.0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '13.0'
26
+ description: Initialize, backup, and restore files with a YAML config
27
+ email:
28
+ - castled@example.com
29
+ executables:
30
+ - simple-backup
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - Gemfile
35
+ - Gemfile.lock
36
+ - README.md
37
+ - Rakefile
38
+ - castled.gemspec
39
+ - docs/index.html
40
+ - exe/simple-backup
41
+ - lib/castled.rb
42
+ - lib/castled/backup.rb
43
+ - lib/castled/cli.rb
44
+ - lib/castled/config.rb
45
+ - lib/castled/version.rb
46
+ homepage: https://github.com/example/castled
47
+ licenses:
48
+ - MIT
49
+ metadata: {}
50
+ rdoc_options: []
51
+ require_paths:
52
+ - lib
53
+ required_ruby_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: 3.0.0
58
+ required_rubygems_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ requirements: []
64
+ rubygems_version: 4.0.6
65
+ specification_version: 4
66
+ summary: Simple backups via simple-backup CLI
67
+ test_files: []