dotsync 0.3.2 → 0.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0c385c8eed3cda30997c16a18593919ccd91e66cb9867c6426b9754766feec1e
4
- data.tar.gz: 0636ec7e59bb3abe38547d66090305ce23035a0297b2edcdaff7e24b132ba691
3
+ metadata.gz: 3b49b79875a41ac8a64361efc61108dc18ee203b08804daf40e709e18972ed88
4
+ data.tar.gz: f3eaf2a153a9757d98018900245ac7020596aab2844dbf1841d99e5e7ae055c3
5
5
  SHA512:
6
- metadata.gz: 2f8d30e519f03d03049bfe5ac0f2816ff0208ec445a259ceaa0c617daf9a8f3a5e46ac562fce617ff42fe238bcdd7486127a1ed5294fd971a4ff920db469bba6
7
- data.tar.gz: 6c6eeb9add7b30806722c482f085ce76747399908ac0cd298a16d998e130ffff02b1f30af4f8478e10321e3e1d7882f6cea7a891c0c8c9664ac2c4e6d6b6f9a1
6
+ metadata.gz: 3315fa7273357e2a61c494e1fa3abdd31ba2e43089888f8dc5174e8bc70ce69fe4bbf553a3db2d647d437b9dae81811056dcc43f7d31bef56736346628ef76b1
7
+ data.tar.gz: fa0bdc870da7374523db60c823d527e0ec8c1da8809548b3f4aeaaec03872cf3f96e08bff0c27d73524c80677ef6e765725416d74b57f83d66f57f86cfb31772
data/CHANGELOG.md CHANGED
@@ -1,3 +1,25 @@
1
+ ## [0.4.0] - 2026-03-01
2
+
3
+ ### Added
4
+
5
+ - Add manifest-based orphan cleanup for non-force sync mappings (#32)
6
+ - Add `source` directive for config file indirection (#29) (#30)
7
+
8
+ ### Changed
9
+
10
+ - CHANGELOG: remove manual version
11
+ - Decouple Terminal::Table from domain logic via TableRenderer (#28)
12
+
13
+ ## [0.3.3] - 2026-02-16
14
+
15
+ ### Added
16
+
17
+ - Add --force-hooks flag to re-run hooks when no files changed (#25) (#26)
18
+
19
+ ### Changed
20
+
21
+ - Update version
22
+
1
23
  ## [0.3.2] - 2026-02-14
2
24
 
3
25
  ### Fixed
@@ -451,5 +473,7 @@ Add gem executables
451
473
 
452
474
  Initial version
453
475
 
476
+ [0.4.0]: https://github.com/dsaenztagarro/dotsync/compare/v0.3.3...v0.4.0
477
+ [0.3.3]: https://github.com/dsaenztagarro/dotsync/compare/v0.3.2...v0.3.3
454
478
  [0.3.2]: https://github.com/dsaenztagarro/dotsync/compare/v0.3.1...v0.3.2
455
479
  [0.3.1]: https://github.com/dsaenztagarro/dotsync/compare/v0.3.0...v0.3.1
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- dotsync (0.3.2)
4
+ dotsync (0.4.0)
5
5
  fileutils (~> 1.7.3)
6
6
  find (~> 0.2.0)
7
7
  listen (~> 3.9.0)
data/README.md CHANGED
@@ -20,6 +20,7 @@ Dotsync is a powerful Ruby gem for managing and synchronizing your dotfiles acro
20
20
  - **Automatic Backups**: Pull operations create timestamped backups for easy recovery
21
21
  - **Live Watching**: Continuously monitor and sync changes in real-time with `watch` command
22
22
  - **Config Includes**: Compose configs from a shared base + machine-specific overlays with `include`
23
+ - **Config Source**: Point local config to your dotfiles repo with `source` — changes are visible immediately without syncing
23
24
  - **Post-Sync Hooks**: Run commands automatically after files change (e.g., codesigning, chmod, service reload)
24
25
  - **Customizable Output**: Control verbosity and customize icons to match your preferences
25
26
  - **Auto-Updates**: Get notified when new versions are available
@@ -37,6 +38,7 @@ Dotsync is a powerful Ruby gem for managing and synchronizing your dotfiles acro
37
38
  - [Mapping Options (force, only, ignore)](#force-only-and-ignore-options-in-mappings)
38
39
  - [Post-Sync Hooks](#post-sync-hooks)
39
40
  - [Config Includes](#config-includes)
41
+ - [Config Source](#config-source)
40
42
  - [Safety Features](#safety-features)
41
43
  - [Customizing Icons](#customizing-icons)
42
44
  - [Automatic Update Checks](#automatic-update-checks)
@@ -627,6 +629,54 @@ paths = ["~/.config/nvim/", "~/.config/zsh/"]
627
629
  - The `include` key is consumed and does not appear in the merged config
628
630
  - Cache invalidation tracks both the overlay and included file's mtime/size
629
631
 
632
+ #### Config Source
633
+
634
+ Use `source` to point your local config at the authoritative copy in your dotfiles repository. Instead of syncing config files back and forth, dotsync reads the config directly from the repo. Changes are visible immediately — no pull needed.
635
+
636
+ **The problem it solves:** When config files change in your dotfiles repo, you normally need to `ds pull` to update local copies, then run `ds pull` again so the new config takes effect. With `source`, dotsync reads from the repo directly, eliminating this two-pass problem.
637
+
638
+ **Setup:**
639
+
640
+ ```toml
641
+ # ~/.config/dotsync.toml (one-time setup — never changes)
642
+ source = "$XDG_CONFIG_HOME_MIRROR/dotsync/dotsync.mbp_personal.toml"
643
+ ```
644
+
645
+ ```toml
646
+ # ~/Code/dotfiles/xdg_config_home/dotsync/dotsync.mbp_personal.toml (the real config)
647
+ include = "dotsync.base.toml"
648
+
649
+ [[sync.xdg_config]]
650
+ path = "zsh"
651
+ ignore = [".zsh_sessions", ".zsh_history", ".zcompdump"]
652
+
653
+ [[sync.xdg_config]]
654
+ path = "nvim"
655
+ force = true
656
+ ignore = ["lazy-lock.json"]
657
+ ```
658
+
659
+ The local file is a thin pointer that never changes. The real config (and its `include` base) live in the repo and are read directly.
660
+
661
+ **Rules:**
662
+ - `source` must be the **only** key in the pointer file — no other config keys allowed
663
+ - The source file can use `include` to compose with a base config (resolved relative to the source file)
664
+ - Chained sources (a source file pointing to another source) are not supported
665
+ - Environment variables are expanded in the `source` path (e.g., `$XDG_CONFIG_HOME_MIRROR`)
666
+ - Cache invalidation tracks the pointer file, the source file, and any included file
667
+
668
+ **Per-machine setup:**
669
+
670
+ ```
671
+ dotfiles/xdg_config_home/dotsync/
672
+ dotsync.base.toml # shared mappings, hooks, watch paths
673
+ dotsync.mbp_personal.toml # include + personal-only mappings
674
+ dotsync.mbp_work.toml # include + work-only mappings
675
+ dotsync.mac_mini.toml # include + mac-mini-only mappings
676
+ ```
677
+
678
+ Each machine's `~/.config/dotsync.toml` is a one-line pointer to its overlay. Edit the repo files and changes take effect immediately — no syncing, no two-pass.
679
+
630
680
  ### Safety Features
631
681
 
632
682
  Dotsync includes several safety mechanisms to prevent accidental data loss:
data/exe/dotsync CHANGED
@@ -52,6 +52,7 @@ opt_parser = OptionParser.new do |opts|
52
52
  --only-mappings Show only the mappings section
53
53
  -v, --verbose Force showing all available information
54
54
  --diff-content Show git-like content diff for modified files
55
+ --force-hooks Run hooks even when no files changed
55
56
  --trace Show full error backtraces (for debugging)
56
57
  --version Show version number
57
58
  -h, --help Show this help message
@@ -117,6 +118,10 @@ opt_parser = OptionParser.new do |opts|
117
118
  options[:diff_content] = true
118
119
  end
119
120
 
121
+ opts.on("--force-hooks", "Run hooks even when no files changed") do
122
+ options[:force_hooks] = true
123
+ end
124
+
120
125
  opts.on("--trace", "Show full error backtraces (for debugging)") do
121
126
  options[:trace] = true
122
127
  end
@@ -23,6 +23,7 @@ module Dotsync
23
23
  def show_options(options)
24
24
  info("Options:", icon: :options)
25
25
  logger.log(" Apply: #{options[:apply] ? "TRUE" : "FALSE"}")
26
+ logger.log(" Force hooks: TRUE") if options[:force_hooks]
26
27
  logger.log("")
27
28
  end
28
29
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "find"
4
+
3
5
  module Dotsync
4
6
  # MappingsTransfer provides shared functionality for push/pull actions.
5
7
  #
@@ -46,15 +48,13 @@ module Dotsync
46
48
  info("Environment variables:", icon: :env_vars)
47
49
 
48
50
  rows = env_vars.map { |env_var| [env_var, ENV[env_var]] }.sort_by(&:first)
49
- table = Terminal::Table.new(rows: rows)
50
- logger.log(table)
51
+ logger.log(Dotsync::TableRenderer.new(rows: rows).render)
51
52
  logger.log("")
52
53
  end
53
54
 
54
55
  def show_mappings_legend
55
56
  info("Mappings Legend:", icon: :legend)
56
- table = Terminal::Table.new(rows: MAPPINGS_LEGEND)
57
- logger.log(table)
57
+ logger.log(Dotsync::TableRenderer.new(rows: MAPPINGS_LEGEND).render)
58
58
  logger.log("")
59
59
  end
60
60
 
@@ -68,15 +68,13 @@ module Dotsync
68
68
  colorize_env_vars(mapping.original_dest)
69
69
  ]
70
70
  end
71
- table = Terminal::Table.new(headings: ["Flags", "Source", "Destination"], rows: rows)
72
- logger.log(table)
71
+ logger.log(Dotsync::TableRenderer.new(headings: ["Flags", "Source", "Destination"], rows: rows).render)
73
72
  logger.log("")
74
73
  end
75
74
 
76
75
  def show_differences_legend
77
76
  info("Differences Legend:", icon: :legend)
78
- table = Terminal::Table.new(rows: DIFFERENCES_LEGEND)
79
- logger.log(table)
77
+ logger.log(Dotsync::TableRenderer.new(rows: DIFFERENCES_LEGEND).render)
80
78
  logger.log("")
81
79
  end
82
80
 
@@ -95,6 +93,8 @@ module Dotsync
95
93
  logger.log("")
96
94
 
97
95
  show_content_diffs if diff_content && has_modifications?
96
+
97
+ show_orphan_preview if respond_to?(:manifests_xdg_data_home, true)
98
98
  end
99
99
 
100
100
  def show_content_diffs
@@ -136,20 +136,47 @@ module Dotsync
136
136
  end
137
137
  end
138
138
 
139
- def execute_hooks
139
+ def cleanup_orphans
140
+ valid_mappings.each do |mapping|
141
+ next unless mapping.has_inclusions? && !mapping.force? && mapping.manifest_key
142
+
143
+ current_files = dest_files_matching_inclusions(mapping)
144
+ manifest = Dotsync::Manifest.new(
145
+ dest_dir: mapping.dest,
146
+ key: mapping.manifest_key,
147
+ xdg_data_home: manifests_xdg_data_home
148
+ )
149
+
150
+ manifest.orphans(current_files).each do |orphan_path|
151
+ next unless File.exist?(orphan_path)
152
+
153
+ FileUtils.rm(orphan_path)
154
+ logger.log("#{Icons.diff_removed}#{orphan_path}", color: Colors.diff_removals)
155
+ end
156
+
157
+ manifest.write(current_files)
158
+ end
159
+ end
160
+
161
+ def execute_hooks(force: false)
140
162
  valid_mappings.each_with_index do |mapping, idx|
141
163
  next unless mapping.has_hooks?
142
164
 
143
165
  differ = differs[idx]
144
166
  changed_files = differ.additions + differ.modifications
145
- next if changed_files.empty?
167
+ if changed_files.empty?
168
+ next unless force
169
+
170
+ changed_files = all_dest_files(mapping)
171
+ next if changed_files.empty?
172
+ end
146
173
 
147
174
  runner = Dotsync::HookRunner.new(mapping: mapping, changed_files: changed_files, logger: logger)
148
175
  runner.execute
149
176
  end
150
177
  end
151
178
 
152
- def show_hooks_preview
179
+ def show_hooks_preview(force: false)
153
180
  hooks_to_run = []
154
181
 
155
182
  valid_mappings.each_with_index do |mapping, idx|
@@ -157,7 +184,12 @@ module Dotsync
157
184
 
158
185
  differ = differs[idx]
159
186
  changed_files = differ.additions + differ.modifications
160
- next if changed_files.empty?
187
+ if changed_files.empty?
188
+ next unless force
189
+
190
+ changed_files = all_dest_files(mapping)
191
+ next if changed_files.empty?
192
+ end
161
193
 
162
194
  runner = Dotsync::HookRunner.new(mapping: mapping, changed_files: changed_files, logger: logger)
163
195
  hooks_to_run.concat(runner.preview)
@@ -217,5 +249,58 @@ module Dotsync
217
249
  def valid_mappings
218
250
  mappings.select(&:valid?)
219
251
  end
252
+
253
+ def show_orphan_preview
254
+ orphans = []
255
+ valid_mappings.each do |mapping|
256
+ next unless mapping.has_inclusions? && !mapping.force? && mapping.manifest_key
257
+
258
+ current_files = dest_files_matching_inclusions(mapping)
259
+ manifest = Dotsync::Manifest.new(
260
+ dest_dir: mapping.dest,
261
+ key: mapping.manifest_key,
262
+ xdg_data_home: manifests_xdg_data_home
263
+ )
264
+ orphans.concat(manifest.orphans(current_files).select { |p| File.exist?(p) })
265
+ end
266
+
267
+ return if orphans.empty?
268
+
269
+ info("Orphans to remove:", icon: :diff)
270
+ orphans.sort.each do |path|
271
+ logger.log("#{Icons.diff_removed}#{path}", color: Colors.diff_removals)
272
+ end
273
+ logger.log("")
274
+ end
275
+
276
+ def dest_files_matching_inclusions(mapping)
277
+ return [] unless File.directory?(mapping.dest)
278
+
279
+ files = []
280
+ Find.find(mapping.dest) do |path|
281
+ next if path == mapping.dest
282
+ next if File.directory?(path)
283
+ next unless mapping.include?(path)
284
+
285
+ files << Pathname.new(path).relative_path_from(Pathname.new(mapping.dest)).to_s
286
+ end
287
+ files
288
+ end
289
+
290
+ def all_dest_files(mapping)
291
+ if File.directory?(mapping.dest)
292
+ files = []
293
+ Find.find(mapping.dest) do |path|
294
+ next if File.directory?(path)
295
+
296
+ files << path
297
+ end
298
+ files
299
+ elsif File.file?(mapping.dest)
300
+ [mapping.dest]
301
+ else
302
+ []
303
+ end
304
+ end
220
305
  end
221
306
  end
@@ -6,6 +6,7 @@ module Dotsync
6
6
  include OutputSections
7
7
 
8
8
  def_delegator :@config, :backups_root
9
+ def_delegator :@config, :manifests_xdg_data_home
9
10
 
10
11
  def execute(options = {})
11
12
  output_sections = compute_output_sections(options)
@@ -16,7 +17,7 @@ module Dotsync
16
17
  show_mappings if output_sections[:mappings]
17
18
  show_differences_legend if has_differences? && output_sections[:differences_legend]
18
19
  show_differences(diff_content: output_sections[:diff_content]) if output_sections[:differences]
19
- show_hooks_preview if output_sections[:differences]
20
+ show_hooks_preview(force: options[:force_hooks]) if output_sections[:differences]
20
21
 
21
22
  return unless options[:apply]
22
23
 
@@ -33,7 +34,8 @@ module Dotsync
33
34
  end
34
35
 
35
36
  transfer_mappings
36
- execute_hooks
37
+ cleanup_orphans
38
+ execute_hooks(force: options[:force_hooks])
37
39
  action("Mappings pulled", icon: :done)
38
40
  end
39
41
 
@@ -14,7 +14,7 @@ module Dotsync
14
14
  show_mappings if output_sections[:mappings]
15
15
  show_differences_legend if has_differences? && output_sections[:differences_legend]
16
16
  show_differences(diff_content: output_sections[:diff_content]) if output_sections[:differences]
17
- show_hooks_preview if output_sections[:differences]
17
+ show_hooks_preview(force: options[:force_hooks]) if output_sections[:differences]
18
18
 
19
19
  return unless options[:apply]
20
20
 
@@ -24,7 +24,7 @@ module Dotsync
24
24
  end
25
25
 
26
26
  transfer_mappings
27
- execute_hooks
27
+ execute_hooks(force: options[:force_hooks])
28
28
  action("Mappings pushed", icon: :done)
29
29
  end
30
30
  end
@@ -147,6 +147,7 @@ module Dotsync
147
147
  base["force"] = mapping["force"] if mapping.key?("force")
148
148
  base["ignore"] = mapping["ignore"] if mapping.key?("ignore")
149
149
  base["only"] = only if only
150
+ base["sync_type"] = shorthand_type
150
151
 
151
152
  # Resolve hooks for direction
152
153
  if mapping.key?("hooks")
@@ -13,6 +13,10 @@ module Dotsync
13
13
  File.join(xdg_data_home, "dotsync", "backups")
14
14
  end
15
15
 
16
+ def manifests_xdg_data_home
17
+ xdg_data_home
18
+ end
19
+
16
20
  private
17
21
  SECTION_NAME = "pull"
18
22
 
@@ -5,7 +5,7 @@ require_relative "../core"
5
5
 
6
6
  # Gems needed for pull
7
7
  require "toml-rb"
8
- require "terminal-table"
8
+ require_relative "../utils/table_renderer"
9
9
 
10
10
  # Utils needed for pull
11
11
  require_relative "../utils/file_transfer"
@@ -14,6 +14,7 @@ require_relative "../utils/config_cache"
14
14
  require_relative "../utils/content_diff"
15
15
  require_relative "../utils/parallel"
16
16
  require_relative "../utils/hook_runner"
17
+ require_relative "../utils/manifest"
17
18
 
18
19
  # Models
19
20
  require_relative "../models/mapping"
@@ -5,7 +5,7 @@ require_relative "../core"
5
5
 
6
6
  # Gems needed for push
7
7
  require "toml-rb"
8
- require "terminal-table"
8
+ require_relative "../utils/table_renderer"
9
9
 
10
10
  # Utils needed for push
11
11
  require_relative "../utils/file_transfer"
@@ -21,7 +21,7 @@ module Dotsync
21
21
  class Mapping
22
22
  include Dotsync::PathUtils
23
23
 
24
- attr_reader :original_src, :original_dest, :original_ignores, :original_only
24
+ attr_reader :original_src, :original_dest, :original_ignores, :original_only, :sync_type
25
25
 
26
26
  def initialize(attributes)
27
27
  @original_src = attributes["src"]
@@ -30,6 +30,7 @@ module Dotsync
30
30
  @original_only = Array(attributes["only"])
31
31
  @force = attributes["force"] || false
32
32
  @hooks = Array(attributes["hooks"])
33
+ @sync_type = attributes["sync_type"]
33
34
 
34
35
  @sanitized_src, @sanitized_dest, @sanitized_ignores, @sanitized_only = process_paths(
35
36
  @original_src,
@@ -65,6 +66,27 @@ module Dotsync
65
66
  @hooks.any?
66
67
  end
67
68
 
69
+ def has_inclusions?
70
+ @original_only.any?
71
+ end
72
+
73
+ def manifest_key
74
+ return nil unless @sync_type
75
+
76
+ shorthand_base = SyncMappings::SHORTHANDS.dig(@sync_type, :local)
77
+ return @sync_type unless shorthand_base
78
+
79
+ expanded_base = sanitize_path(shorthand_base)
80
+ if dest == expanded_base
81
+ @sync_type
82
+ elsif dest.start_with?("#{expanded_base}/")
83
+ subpath = dest.delete_prefix("#{expanded_base}/")
84
+ "#{@sync_type}--#{subpath}"
85
+ else
86
+ @sync_type
87
+ end
88
+ end
89
+
68
90
  def directories?
69
91
  File.directory?(src) && File.directory?(dest)
70
92
  end
@@ -211,10 +233,6 @@ module Dotsync
211
233
  @original_ignores.any?
212
234
  end
213
235
 
214
- def has_inclusions?
215
- @original_only.any?
216
- end
217
-
218
236
  def process_paths(raw_src, raw_dest, raw_ignores, raw_only)
219
237
  sanitized_src = sanitize_path(raw_src)
220
238
  sanitized_dest = sanitize_path(raw_dest)
@@ -7,6 +7,7 @@ require_relative "config_merger"
7
7
  module Dotsync
8
8
  class ConfigCache
9
9
  include Dotsync::XDGBaseDirectory
10
+ include Dotsync::PathUtils
10
11
 
11
12
  def initialize(config_path)
12
13
  @config_path = File.expand_path(config_path)
@@ -50,6 +51,15 @@ module Dotsync
50
51
  cache_age_days = (Time.now.to_f - meta["cached_at"]) / 86400
51
52
  return false if cache_age_days > 7
52
53
 
54
+ # Check source file validity if present
55
+ if meta["source_file_path"]
56
+ return false unless File.exist?(meta["source_file_path"])
57
+
58
+ source_file_stat = File.stat(meta["source_file_path"])
59
+ return false if source_file_stat.mtime.to_f != meta["source_file_mtime"]
60
+ return false if source_file_stat.size != meta["source_file_size"]
61
+ end
62
+
53
63
  # Check include file validity if present
54
64
  if meta["include_path"]
55
65
  return false unless File.exist?(meta["include_path"])
@@ -82,13 +92,62 @@ module Dotsync
82
92
 
83
93
  def resolve_config
84
94
  raw = parse_toml
85
- @merger = ConfigMerger.new(raw, @config_path)
95
+ if raw.key?("source")
96
+ resolve_source(raw)
97
+ else
98
+ @merger = ConfigMerger.new(raw, @config_path)
99
+ @merger.resolve
100
+ end
101
+ end
102
+
103
+ def resolve_source(raw)
104
+ validate_source!(raw)
105
+ @source_path = resolve_source_path(raw["source"])
106
+ validate_source_exists!
107
+
108
+ source_raw = parse_toml_file(@source_path)
109
+ validate_no_chained_source!(source_raw)
110
+
111
+ @merger = ConfigMerger.new(source_raw, @source_path)
86
112
  @merger.resolve
87
113
  end
88
114
 
115
+ def validate_source!(raw)
116
+ unless raw["source"].is_a?(String)
117
+ raise ConfigError, "Config Error: 'source' must be a string path, got #{raw["source"].class}"
118
+ end
119
+
120
+ if raw.keys.any? { |k| k != "source" }
121
+ raise ConfigError,
122
+ "Config Error: 'source' cannot be combined with other keys. " \
123
+ "The source file should contain the full configuration."
124
+ end
125
+ end
126
+
127
+ def resolve_source_path(source_value)
128
+ expanded = expand_env_vars(source_value)
129
+ File.expand_path(expanded)
130
+ end
131
+
132
+ def validate_source_exists!
133
+ unless File.exist?(@source_path)
134
+ raise ConfigError, "Config Error: Source file not found: #{@source_path}"
135
+ end
136
+ end
137
+
138
+ def validate_no_chained_source!(config)
139
+ if config.key?("source")
140
+ raise ConfigError, "Config Error: Chained sources are not supported (found 'source' in #{@source_path})"
141
+ end
142
+ end
143
+
89
144
  def parse_toml
145
+ parse_toml_file(@config_path)
146
+ end
147
+
148
+ def parse_toml_file(path)
90
149
  require "toml-rb"
91
- TomlRB.load_file(@config_path)
150
+ TomlRB.load_file(path)
92
151
  end
93
152
 
94
153
  def build_metadata
@@ -101,6 +160,13 @@ module Dotsync
101
160
  dotsync_version: Dotsync::VERSION
102
161
  }
103
162
 
163
+ if @source_path
164
+ source_file_stat = File.stat(@source_path)
165
+ meta[:source_file_path] = @source_path
166
+ meta[:source_file_mtime] = source_file_stat.mtime.to_f
167
+ meta[:source_file_size] = source_file_stat.size
168
+ end
169
+
104
170
  if @merger&.include_path
105
171
  include_stat = File.stat(@merger.include_path)
106
172
  meta[:include_path] = @merger.include_path
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Dotsync
6
+ class Manifest
7
+ MANIFESTS_DIR = "dotsync/manifests"
8
+
9
+ # @param dest_dir [String] the mapping's destination directory (absolute)
10
+ # @param key [String] manifest filename key (e.g., "xdg_bin")
11
+ # @param xdg_data_home [String] base path for manifest storage
12
+ def initialize(dest_dir:, key:, xdg_data_home:)
13
+ @dest_dir = dest_dir
14
+ @key = key
15
+ @manifest_path = File.join(xdg_data_home, MANIFESTS_DIR, "#{key}.json")
16
+ end
17
+
18
+ # Returns array of relative file paths from stored manifest
19
+ # @return [Array<String>]
20
+ def read
21
+ return [] unless File.exist?(@manifest_path)
22
+
23
+ data = JSON.parse(File.read(@manifest_path))
24
+ Array(data["files"])
25
+ rescue JSON::ParserError
26
+ []
27
+ end
28
+
29
+ # Writes current file list to manifest
30
+ # @param files [Array<String>] relative file paths
31
+ def write(files)
32
+ FileUtils.mkdir_p(File.dirname(@manifest_path))
33
+ File.write(@manifest_path, JSON.pretty_generate({ "files" => files.sort }))
34
+ end
35
+
36
+ # Returns orphan absolute paths: files in old manifest but not in current_files
37
+ # @param current_files [Array<String>] relative file paths currently synced
38
+ # @return [Array<String>] absolute paths ready for deletion
39
+ def orphans(current_files)
40
+ previous = read
41
+ orphaned = previous - current_files
42
+ orphaned.map { |file| File.join(@dest_dir, file) }
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "terminal-table"
4
+
5
+ module Dotsync
6
+ class TableRenderer
7
+ attr_reader :rows
8
+
9
+ def initialize(rows:, headings: nil)
10
+ @rows = rows
11
+ @headings = headings
12
+ end
13
+
14
+ def render
15
+ options = { rows: @rows }
16
+ options[:headings] = @headings if @headings
17
+ Terminal::Table.new(**options).to_s
18
+ end
19
+ end
20
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dotsync
4
- VERSION = "0.3.2"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/dotsync.rb CHANGED
@@ -11,7 +11,7 @@ require "logger"
11
11
  require "forwardable" # Ruby standard library
12
12
  require "ostruct"
13
13
  require "find"
14
- require "terminal-table"
14
+ require_relative "dotsync/utils/table_renderer"
15
15
 
16
16
  # Base classes
17
17
  require_relative "dotsync/errors"
@@ -34,6 +34,7 @@ require_relative "dotsync/utils/config_cache"
34
34
  require_relative "dotsync/utils/config_merger"
35
35
  require_relative "dotsync/utils/parallel"
36
36
  require_relative "dotsync/utils/hook_runner"
37
+ require_relative "dotsync/utils/manifest"
37
38
 
38
39
  # Models
39
40
  require_relative "dotsync/models/mapping"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dotsync
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Sáenz
@@ -340,8 +340,10 @@ files:
340
340
  - lib/dotsync/utils/file_transfer.rb
341
341
  - lib/dotsync/utils/hook_runner.rb
342
342
  - lib/dotsync/utils/logger.rb
343
+ - lib/dotsync/utils/manifest.rb
343
344
  - lib/dotsync/utils/parallel.rb
344
345
  - lib/dotsync/utils/path_utils.rb
346
+ - lib/dotsync/utils/table_renderer.rb
345
347
  - lib/dotsync/utils/version_checker.rb
346
348
  - lib/dotsync/version.rb
347
349
  homepage: https://github.com/dsaenztagarro/dotsync