appydave-tools 0.37.0 → 0.38.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,342 @@
1
+ # Code Quality Analysis - Last 7 Days (Nov 14-21, 2025)
2
+
3
+ ## Summary
4
+ - **Commits analyzed:** 30 commits
5
+ - **Files changed:** 29 unique files
6
+ - **Key areas:** DAM CLI, Configuration system, Project resolution, S3 operations
7
+ - **Analysis date:** 2025-01-21
8
+
9
+ ### Commit Themes
10
+ The last week focused heavily on:
11
+ 1. **Configuration loading fixes** (7x config load calls - commit 94d3ea0)
12
+ 2. **Brand resolution** (v- prefix handling)
13
+ 3. **DAM CLI UX improvements** (validation, table formatting, status displays)
14
+ 4. **Debug infrastructure** (DAM_DEBUG logging for remote debugging)
15
+ 5. **Project resolver** (regex capture group bug fix - commit 9e49668)
16
+
17
+ ## Critical Issues 🔴
18
+
19
+ ### 1. Code Duplication: Git Operations Helper Methods
20
+ - **Found in:**
21
+ - `lib/appydave/tools/dam/status.rb:243-275` (4 methods)
22
+ - `lib/appydave/tools/dam/repo_status.rb:109-144` (6 methods)
23
+ - `lib/appydave/tools/dam/repo_push.rb:107-119` (3 methods)
24
+
25
+ - **Description:** Three classes have nearly identical git helper methods:
26
+ ```ruby
27
+ # DUPLICATED across Status, RepoStatus, RepoPush:
28
+ def current_branch
29
+ `git -C "#{brand_path}" rev-parse --abbrev-ref HEAD 2>/dev/null`.strip
30
+ end
31
+
32
+ def commits_ahead
33
+ `git -C "#{brand_path}" rev-list --count @{upstream}..HEAD 2>/dev/null`.strip.to_i
34
+ end
35
+
36
+ def commits_behind
37
+ `git -C "#{brand_path}" rev-list --count HEAD..@{upstream} 2>/dev/null`.strip.to_i
38
+ end
39
+
40
+ # Plus: remote_url, modified_files_count, untracked_files_count, uncommitted_changes?
41
+ ```
42
+
43
+ - **Impact:**
44
+ - **High maintenance burden** - Git command changes require updates in 3 places
45
+ - **Bug risk** - Bug fixes might miss one location
46
+ - **~90 lines duplicated** across 3 files
47
+ - **Testing burden** - Same behavior tested 3 times
48
+
49
+ - **Recommendation:**
50
+ - Extract to `lib/appydave/tools/dam/git_helper.rb` module:
51
+ ```ruby
52
+ module Appydave::Tools::Dam::GitHelper
53
+ def current_branch(repo_path)
54
+ `git -C "#{repo_path}" rev-parse --abbrev-ref HEAD 2>/dev/null`.strip
55
+ end
56
+ # ... all other git methods
57
+ end
58
+ ```
59
+ - Mix into classes: `include GitHelper`
60
+ - Call: `current_branch(brand_path)`
61
+
62
+ - **Estimated effort:** Medium (2-3 hours including tests)
63
+
64
+ ### 2. Code Duplication: Configuration Loading Pattern
65
+ - **Found in:**
66
+ - `lib/appydave/tools/dam/config.rb` - 7 calls to `Config.configure`
67
+ - `lib/appydave/tools/dam/project_resolver.rb:144`
68
+ - `lib/appydave/tools/dam/status.rb:32`
69
+ - `lib/appydave/tools/dam/repo_status.rb:36,55`
70
+ - `lib/appydave/tools/dam/share_operations.rb:68`
71
+ - Plus 15+ more locations across DAM module
72
+
73
+ - **Description:** `Appydave::Tools::Configuration::Config.configure` called repeatedly, often multiple times in the same method or class
74
+
75
+ - **Pattern Example:**
76
+ ```ruby
77
+ # lib/appydave/tools/dam/config.rb:27-29
78
+ def brand_path(brand_key)
79
+ Appydave::Tools::Configuration::Config.configure # ← Called
80
+ brand_info = Appydave::Tools::Configuration::Config.brands.get_brand(brand_key)
81
+ # ...
82
+ end
83
+
84
+ # lib/appydave/tools/dam/config.rb:51-52
85
+ def project_path(brand_key, project_id)
86
+ Appydave::Tools::Configuration::Config.configure # ← Called again
87
+ brand_info = Appydave::Tools::Configuration::Config.brands.get_brand(brand_key)
88
+ # ...
89
+ end
90
+ ```
91
+
92
+ - **Impact:**
93
+ - **Performance concern** - Config loaded 7+ times in tight loops (commit 94d3ea0 fixed this)
94
+ - **Code noise** - Reduces readability with repeated boilerplate
95
+ - **Unclear responsibility** - Is `Config.configure` idempotent? (Yes, but not obvious)
96
+
97
+ - **Recommendation:**
98
+ - **Option A (Quick fix):** Call once per class in initializer/module load
99
+ - **Option B (Better):** Make `Config.configure` memoized singleton (already done in commit 94d3ea0)
100
+ - **Option C (Best):** Dependency injection - pass config objects instead of calling global
101
+
102
+ - **Estimated effort:** Small (already partially fixed in 94d3ea0)
103
+
104
+ ### 3. Architectural Issue: Brand Resolution Logic Scattered
105
+ - **Found in:**
106
+ - `lib/appydave/tools/dam/config.rb:91-116` (`expand_brand` method)
107
+ - `lib/appydave/tools/dam/project_resolver.rb:118-121` (regex capture + `.sub(/^v-/, '')`)
108
+ - `bin/dam` - Multiple inline brand validation checks
109
+
110
+ - **Description:** Brand key transformation (`appydave` ↔ `v-appydave`) handled in multiple places with slightly different logic:
111
+ ```ruby
112
+ # Config.rb - lookup from brands.json
113
+ brand = brands_config.brands.find { |b| b.key.downcase == shortcut_str.downcase }
114
+ return "v-#{brand.key}" if brand
115
+
116
+ # ProjectResolver.rb - strip v- prefix
117
+ brand_key = brand_with_prefix.sub(/^v-/, '')
118
+
119
+ # Hardcoded shortcuts in Config.rb:111-114
120
+ case normalized
121
+ when 'joy' then 'v-beauty-and-joy'
122
+ when 'ss' then 'v-supportsignal'
123
+ ```
124
+
125
+ - **Impact:**
126
+ - **Fragile** - Adding new brand shortcuts requires changes in multiple files
127
+ - **Inconsistent** - Some code assumes v- prefix, some doesn't
128
+ - **Hard to test** - Logic spread across multiple layers
129
+ - **Bug history** - Recent fixes (commits 94d3ea0, 771a20f, 3966f16) show ongoing confusion
130
+
131
+ - **Recommendation:**
132
+ - Create `BrandResolver` class with clear responsibilities:
133
+ ```ruby
134
+ class BrandResolver
135
+ def self.expand(shortcut) # appydave → v-appydave
136
+ def self.normalize(brand) # v-appydave → appydave
137
+ def self.validate(brand) # raises if invalid
138
+ def self.to_config_key(brand) # always returns key form (no v-)
139
+ def self.to_display(brand) # always returns display form (v-)
140
+ end
141
+ ```
142
+ - Centralize all brand name transformations
143
+ - Document which methods expect which format
144
+
145
+ - **Estimated effort:** Large (1-2 days including refactor + tests)
146
+
147
+ ## Moderate Issues 🟡
148
+
149
+ ### 1. Pattern Inconsistency: Config Loading in Tests
150
+ - **Found in:**
151
+ - `spec/appydave/tools/dam/project_listing_spec.rb:23-29` (extensive mocking)
152
+ - `spec/appydave/tools/dam/project_resolver_spec.rb:304-307` (partial mocking)
153
+
154
+ - **Pattern A (ProjectListingSpec):** Heavy mocking of Config class methods
155
+ ```ruby
156
+ allow(Appydave::Tools::Dam::Config).to receive_messages(
157
+ projects_root: temp_root,
158
+ available_brands: %w[appydave voz]
159
+ )
160
+ allow(Appydave::Tools::Dam::Config).to receive(:brand_path).with('appydave').and_return(brand1_path)
161
+ # 20 lines of allow() statements
162
+ ```
163
+
164
+ - **Pattern B (ProjectResolverSpec):** Shared context with real filesystem
165
+ ```ruby
166
+ include_context 'with vat filesystem and brands', brands: %w[appydave voz]
167
+ # Uses actual temp directories, minimal mocking
168
+ ```
169
+
170
+ - **Impact:**
171
+ - **Confusion** - New contributors don't know which pattern to use
172
+ - **Test brittleness** - Heavy mocking in ProjectListingSpec (20 mocks vs 252 lines = 8% mock density)
173
+ - **Maintenance burden** - Config changes break many mocks
174
+
175
+ - **Recommendation:**
176
+ - **Standardize on Pattern B** - Real filesystem with shared contexts
177
+ - Refactor `project_listing_spec.rb` to use `with vat filesystem` context
178
+ - Reserve mocking for external services (S3, AWS) not internal DAM classes
179
+
180
+ - **Estimated effort:** Medium (half day to refactor tests)
181
+
182
+ ### 2. Pattern Inconsistency: Error Handling
183
+ - **Found in:**
184
+ - `lib/appydave/tools/dam/project_resolver.rb:19` - `raise '❌ Error message'`
185
+ - `lib/appydave/tools/dam/config_loader.rb:27` - `raise ConfigNotFoundError, <<~ERROR`
186
+ - `lib/appydave/tools/dam/share_operations.rb:175` - `raise ArgumentError, 'message'`
187
+ - `bin/dam` - Multiple `rescue StandardError => e` blocks with `puts` + `exit 1`
188
+
189
+ - **Patterns:**
190
+ - **String errors:** `raise '❌ ...'` (ProjectResolver)
191
+ - **Custom exceptions:** `ConfigNotFoundError` (ConfigLoader)
192
+ - **Standard exceptions:** `ArgumentError` (ShareOperations)
193
+ - **CLI error handling:** `rescue → puts → exit` (bin/dam)
194
+
195
+ - **Recommendation:**
196
+ - Define DAM-specific exception hierarchy:
197
+ ```ruby
198
+ module Appydave::Tools::Dam
199
+ class DamError < StandardError; end
200
+ class BrandNotFoundError < DamError; end
201
+ class ProjectNotFoundError < DamError; end
202
+ class ConfigurationError < DamError; end
203
+ end
204
+ ```
205
+ - Use consistently throughout DAM module
206
+ - Keep CLI-level `rescue StandardError` in bin/dam for user-friendly messages
207
+
208
+ - **Estimated effort:** Small (few hours)
209
+
210
+ ### 3. Missing Abstraction: Directory Size Calculation
211
+ - **Found in:**
212
+ - `lib/appydave/tools/dam/project_listing.rb:154-164`
213
+ - `lib/appydave/tools/dam/s3_operations.rb:770-778`
214
+ - `lib/appydave/tools/dam/sync_from_ssd.rb:270-278` (format_bytes)
215
+
216
+ - **Description:** Same directory size calculation logic duplicated 3+ times
217
+
218
+ - **Recommendation:**
219
+ - Extract to `lib/appydave/tools/dam/file_utils.rb`:
220
+ ```ruby
221
+ module FileUtils
222
+ def self.directory_size(path)
223
+ def self.format_size(bytes)
224
+ end
225
+ ```
226
+
227
+ - **Estimated effort:** Small (1 hour)
228
+
229
+ ## Minor Observations 🔵
230
+
231
+ ### 1. Debugging Infrastructure (GOOD - Protected Pattern ✅)
232
+ - **Found in:**
233
+ - `lib/appydave/tools/configuration/models/config_base.rb:33-58`
234
+ - `lib/appydave/tools/dam/project_resolver.rb:149,153`
235
+ - Multiple `puts "DEBUG: ..."` statements
236
+
237
+ - **Description:** DAM_DEBUG and DEBUG env var support added for remote debugging (commit 8844aa9)
238
+
239
+ - **Impact:** **Positive** - Essential for production debugging
240
+
241
+ - **Recommendation:** **Keep as-is** - This is intentional defensive programming, not code bloat
242
+
243
+ ### 2. Regex Capture Group Bug Fixed
244
+ - **Fixed in:** commit 9e49668
245
+ - **Issue:** `Regexp.last_match` reset by `.sub()` call
246
+ - **Solution:** Capture before transformation
247
+ ```ruby
248
+ project = Regexp.last_match(2) # Capture BEFORE
249
+ brand_key = brand_with_prefix.sub(/^v-/, '') # Then modify
250
+ ```
251
+
252
+ - **Impact:** **Positive** - Proper fix, not hidden with mocks
253
+
254
+ ## Positive Patterns ✅
255
+
256
+ ### 1. Test Organization with Shared Contexts
257
+ - **Found in:** `spec/appydave/tools/dam/project_resolver_spec.rb`
258
+ - **Why it's good:**
259
+ - Uses `include_context 'with vat filesystem and brands'`
260
+ - Real temp directories, minimal mocking
261
+ - Tests actual behavior, not implementation details
262
+ - Easy to understand and maintain
263
+
264
+ - **Recommend:** Use this pattern for all DAM specs
265
+
266
+ ### 2. Configuration Debug Logging
267
+ - **Found in:** `lib/appydave/tools/configuration/models/config_base.rb`
268
+ - **Why it's good:**
269
+ - Toggleable with DAM_DEBUG env var
270
+ - Shows config load path, file existence, parse errors
271
+ - Includes JSON output for verification
272
+ - Essential for remote debugging production issues
273
+
274
+ - **Recommend:** Continue this pattern for all critical configuration paths
275
+
276
+ ### 3. Defensive Nil Checking
277
+ - **Found in:** commit a205319
278
+ - **Example:**
279
+ ```ruby
280
+ def resolve(brand, project_hint)
281
+ raise '❌ Project name is required' if project_hint.nil? || project_hint.empty?
282
+ ```
283
+
284
+ - **Why it's good:** Prevents cryptic `include?` errors on nil
285
+ - **Recommend:** Add similar guards to all public methods expecting non-nil arguments
286
+
287
+ ## Prioritized Action Items
288
+
289
+ ### High Priority (Do First)
290
+ 1. [ ] **Extract GitHelper module** - Eliminate 90 lines of duplication (status.rb, repo_status.rb, repo_push.rb)
291
+ - lib/appydave/tools/dam/git_helper.rb:1
292
+ 2. [ ] **Create BrandResolver abstraction** - Centralize brand transformation logic
293
+ - lib/appydave/tools/dam/brand_resolver.rb:1
294
+ 3. [ ] **Refactor project_listing_spec.rb** - Use shared filesystem context instead of 20 mocks
295
+ - spec/appydave/tools/dam/project_listing_spec.rb:1
296
+
297
+ ### Medium Priority (Do Soon)
298
+ 1. [ ] **Define DAM exception hierarchy** - Replace string raises with typed exceptions
299
+ - lib/appydave/tools/dam/errors.rb:1
300
+ 2. [ ] **Extract FileUtils helpers** - Consolidate directory_size and format_size methods
301
+ - lib/appydave/tools/dam/file_utils.rb:1
302
+ 3. [ ] **Document Config.configure memoization** - Add comment explaining idempotent behavior
303
+ - lib/appydave/tools/configuration/config.rb:15
304
+
305
+ ### Low Priority (Future Improvement)
306
+ 1. [ ] **Add integration tests** - Test brand resolution end-to-end across modules
307
+ 2. [ ] **Extract CLI argument parsing** - bin/dam has 200+ line methods, extract parsers
308
+ 3. [ ] **Consider dependency injection** - Pass config objects instead of global Config calls
309
+
310
+ ## Statistics
311
+ - **Total duplicated code:** ~150 lines (90 git methods + 40 directory utils + 20 config calls)
312
+ - **Test mock density:** ProjectListingSpec: 20 mocks / 252 lines = 8% (acceptable, but could improve)
313
+ - **Pattern inconsistencies:** 3 error handling patterns, 2 test patterns
314
+ - **Large methods:** bin/dam has multiple 50+ line methods (help commands, parsers)
315
+ - **Configuration loading:** Fixed in commit 94d3ea0 (7x redundant calls → memoized singleton)
316
+
317
+ ## Analysis Methodology
318
+
319
+ ### Commands Used:
320
+ ```bash
321
+ # Discovery
322
+ git log --since="7 days ago" --pretty=format:"%h - %s (%an, %ar)"
323
+ git log --since="7 days ago" --name-only | sort | uniq -c | sort -rn
324
+
325
+ # Duplication detection
326
+ grep -rn "def.*brand.*path" lib/appydave/tools/dam/
327
+ grep -rn "git -C.*rev-parse|git -C.*rev-list" lib/appydave/tools/dam/
328
+ grep -c "allow.*to receive|double|instance_double" spec/appydave/tools/dam/*.rb
329
+
330
+ # File analysis
331
+ find lib/appydave/tools/dam -name "*.rb" -exec wc -l {} \;
332
+ ```
333
+
334
+ ### Files Analyzed:
335
+ - 12 DAM module files (config.rb, project_resolver.rb, status.rb, repo_status.rb, s3_operations.rb, etc.)
336
+ - 7 spec files
337
+ - bin/dam CLI script
338
+ - Configuration system files
339
+
340
+ ---
341
+
342
+ **Report Generated:** 2025-01-21 by Claude Code Quality Retrospective Analysis
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appydave
4
+ module Tools
5
+ module Dam
6
+ # Base error for all DAM operations
7
+ class DamError < StandardError; end
8
+
9
+ # Raised when brand directory not found
10
+ class BrandNotFoundError < DamError
11
+ def initialize(brand, available_brands = nil)
12
+ message = "Brand directory not found: #{brand}"
13
+ message += "\n\nAvailable brands:\n#{available_brands}" if available_brands && !available_brands.empty?
14
+ super(message)
15
+ end
16
+ end
17
+
18
+ # Raised when project not found in brand
19
+ class ProjectNotFoundError < DamError; end
20
+
21
+ # Raised when configuration invalid or missing
22
+ class ConfigurationError < DamError; end
23
+
24
+ # Raised when S3 operation fails
25
+ class S3OperationError < DamError; end
26
+
27
+ # Raised when git operation fails
28
+ class GitOperationError < DamError; end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'find'
4
+
5
+ module Appydave
6
+ module Tools
7
+ module Dam
8
+ # File utility methods for DAM operations
9
+ # Provides reusable file and directory helpers
10
+ module FileHelper
11
+ module_function
12
+
13
+ # Calculate total size of directory in bytes
14
+ # @param path [String] Directory path
15
+ # @return [Integer] Size in bytes
16
+ def calculate_directory_size(path)
17
+ return 0 unless Dir.exist?(path)
18
+
19
+ total = 0
20
+ Find.find(path) do |file_path|
21
+ total += File.size(file_path) if File.file?(file_path)
22
+ rescue StandardError
23
+ # Skip files we can't read
24
+ end
25
+ total
26
+ end
27
+
28
+ # Format bytes into human-readable size
29
+ # @param bytes [Integer] Size in bytes
30
+ # @return [String] Formatted size (e.g., "1.5 GB")
31
+ def format_size(bytes)
32
+ return '0 B' if bytes.zero?
33
+
34
+ units = %w[B KB MB GB TB]
35
+ exp = (Math.log(bytes) / Math.log(1024)).to_i
36
+ exp = [exp, units.length - 1].min
37
+
38
+ format('%<size>.1f %<unit>s', size: bytes.to_f / (1024**exp), unit: units[exp])
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appydave
4
+ module Tools
5
+ module Dam
6
+ # Git operations helper for DAM classes
7
+ # Provides reusable git command wrappers
8
+ module GitHelper
9
+ module_function
10
+
11
+ # Get current branch name
12
+ # @param repo_path [String] Path to git repository
13
+ # @return [String] Branch name or 'unknown' if error
14
+ def current_branch(repo_path)
15
+ result = `git -C "#{repo_path}" rev-parse --abbrev-ref HEAD 2>/dev/null`.strip
16
+ result.empty? ? 'unknown' : result
17
+ rescue StandardError
18
+ 'unknown'
19
+ end
20
+
21
+ # Get git remote URL
22
+ # @param repo_path [String] Path to git repository
23
+ # @return [String, nil] Remote URL or nil if not configured
24
+ def remote_url(repo_path)
25
+ result = `git -C "#{repo_path}" remote get-url origin 2>/dev/null`.strip
26
+ result.empty? ? nil : result
27
+ rescue StandardError
28
+ nil
29
+ end
30
+
31
+ # Count commits ahead of remote
32
+ # @param repo_path [String] Path to git repository
33
+ # @return [Integer] Number of commits ahead
34
+ def commits_ahead(repo_path)
35
+ `git -C "#{repo_path}" rev-list --count @{upstream}..HEAD 2>/dev/null`.strip.to_i
36
+ rescue StandardError
37
+ 0
38
+ end
39
+
40
+ # Count commits behind remote
41
+ # @param repo_path [String] Path to git repository
42
+ # @return [Integer] Number of commits behind
43
+ def commits_behind(repo_path)
44
+ `git -C "#{repo_path}" rev-list --count HEAD..@{upstream} 2>/dev/null`.strip.to_i
45
+ rescue StandardError
46
+ 0
47
+ end
48
+
49
+ # Count modified files
50
+ # @param repo_path [String] Path to git repository
51
+ # @return [Integer] Number of modified files
52
+ def modified_files_count(repo_path)
53
+ `git -C "#{repo_path}" status --porcelain 2>/dev/null | grep -E "^.M|^M" | wc -l`.strip.to_i
54
+ rescue StandardError
55
+ 0
56
+ end
57
+
58
+ # Count untracked files
59
+ # @param repo_path [String] Path to git repository
60
+ # @return [Integer] Number of untracked files
61
+ def untracked_files_count(repo_path)
62
+ `git -C "#{repo_path}" status --porcelain 2>/dev/null | grep -E "^\\?\\?" | wc -l`.strip.to_i
63
+ rescue StandardError
64
+ 0
65
+ end
66
+
67
+ # Check if repository has uncommitted changes
68
+ # @param repo_path [String] Path to git repository
69
+ # @return [Boolean] true if changes exist
70
+ def uncommitted_changes?(repo_path)
71
+ system("git -C \"#{repo_path}\" diff-index --quiet HEAD -- 2>/dev/null")
72
+ !$CHILD_STATUS.success?
73
+ rescue StandardError
74
+ false
75
+ end
76
+
77
+ # Fetch from remote
78
+ # @param repo_path [String] Path to git repository
79
+ # @return [Boolean] true if successful
80
+ def fetch(repo_path)
81
+ system("git -C \"#{repo_path}\" fetch 2>/dev/null")
82
+ $CHILD_STATUS.success?
83
+ rescue StandardError
84
+ false
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'find'
4
-
5
3
  # rubocop:disable Style/FormatStringToken
6
4
  # Disabled: Using simple unannotated tokens (%s) for straightforward string formatting
7
5
  # Annotated tokens (%<foo>s) add unnecessary complexity for simple table formatting
@@ -69,7 +67,7 @@ module Appydave
69
67
  # Gather project data
70
68
  project_data = projects.map do |project|
71
69
  project_path = Config.project_path(brand_arg, project)
72
- size = calculate_directory_size(project_path)
70
+ size = FileHelper.calculate_directory_size(project_path)
73
71
  modified = File.mtime(project_path)
74
72
 
75
73
  {
@@ -113,7 +111,7 @@ module Appydave
113
111
  # Gather project data
114
112
  project_data = matches.map do |project|
115
113
  project_path = Config.project_path(brand_arg, project)
116
- size = calculate_directory_size(project_path)
114
+ size = FileHelper.calculate_directory_size(project_path)
117
115
  modified = File.mtime(project_path)
118
116
 
119
117
  {
@@ -146,23 +144,10 @@ module Appydave
146
144
  # Calculate total size of all projects in a brand
147
145
  def self.calculate_total_size(brand, projects)
148
146
  projects.sum do |project|
149
- calculate_directory_size(Config.project_path(brand, project))
147
+ FileHelper.calculate_directory_size(Config.project_path(brand, project))
150
148
  end
151
149
  end
152
150
 
153
- # Calculate size of a directory in bytes
154
- def self.calculate_directory_size(path)
155
- return 0 unless Dir.exist?(path)
156
-
157
- total = 0
158
- Find.find(path) do |file_path|
159
- total += File.size(file_path) if File.file?(file_path)
160
- rescue StandardError
161
- # Skip files we can't read
162
- end
163
- total
164
- end
165
-
166
151
  # Find the most recent modification time across all projects
167
152
  def self.find_last_modified(brand, projects)
168
153
  return Time.at(0) if projects.empty?
@@ -174,13 +159,7 @@ module Appydave
174
159
 
175
160
  # Format size in human-readable format
176
161
  def self.format_size(bytes)
177
- return '0 B' if bytes.zero?
178
-
179
- units = %w[B KB MB GB TB]
180
- exp = (Math.log(bytes) / Math.log(1024)).to_i
181
- exp = [exp, units.length - 1].min
182
-
183
- format('%.1f %s', bytes.to_f / (1024**exp), units[exp])
162
+ FileHelper.format_size(bytes)
184
163
  end
185
164
 
186
165
  # Format date/time in readable format
@@ -105,16 +105,11 @@ module Appydave
105
105
  end
106
106
 
107
107
  def uncommitted_changes?
108
- output = `git -C "#{brand_path}" status --porcelain 2>/dev/null`.strip
109
- !output.empty?
110
- rescue StandardError
111
- false
108
+ GitHelper.uncommitted_changes?(brand_path)
112
109
  end
113
110
 
114
111
  def commits_ahead
115
- `git -C "#{brand_path}" rev-list --count @{upstream}..HEAD 2>/dev/null`.strip.to_i
116
- rescue StandardError
117
- 0
112
+ GitHelper.commits_ahead(brand_path)
118
113
  end
119
114
 
120
115
  def show_push_summary(output)
@@ -107,49 +107,32 @@ module Appydave
107
107
  end
108
108
 
109
109
  def current_branch
110
- `git -C "#{brand_path}" rev-parse --abbrev-ref HEAD 2>/dev/null`.strip
111
- rescue StandardError
112
- 'unknown'
110
+ GitHelper.current_branch(brand_path)
113
111
  end
114
112
 
115
113
  def remote_url
116
- result = `git -C "#{brand_path}" remote get-url origin 2>/dev/null`.strip
117
- result.empty? ? nil : result
118
- rescue StandardError
119
- nil
114
+ GitHelper.remote_url(brand_path)
120
115
  end
121
116
 
122
117
  def modified_files_count
123
- `git -C "#{brand_path}" status --porcelain 2>/dev/null | grep -E "^.M|^M" | wc -l`.strip.to_i
124
- rescue StandardError
125
- 0
118
+ GitHelper.modified_files_count(brand_path)
126
119
  end
127
120
 
128
121
  def untracked_files_count
129
- `git -C "#{brand_path}" status --porcelain 2>/dev/null | grep -E "^\\?\\?" | wc -l`.strip.to_i
130
- rescue StandardError
131
- 0
122
+ GitHelper.untracked_files_count(brand_path)
132
123
  end
133
124
 
134
125
  def commits_ahead
135
- `git -C "#{brand_path}" rev-list --count @{upstream}..HEAD 2>/dev/null`.strip.to_i
136
- rescue StandardError
137
- 0
126
+ GitHelper.commits_ahead(brand_path)
138
127
  end
139
128
 
140
129
  def commits_behind
141
- `git -C "#{brand_path}" rev-list --count HEAD..@{upstream} 2>/dev/null`.strip.to_i
142
- rescue StandardError
143
- 0
130
+ GitHelper.commits_behind(brand_path)
144
131
  end
145
132
 
146
133
  # Check if repo has uncommitted changes (matches old script: git diff-index --quiet HEAD --)
147
134
  def uncommitted_changes?
148
- # git diff-index returns 0 if clean, 1 if there are changes
149
- system("git -C \"#{brand_path}\" diff-index --quiet HEAD -- 2>/dev/null")
150
- !$CHILD_STATUS.success?
151
- rescue StandardError
152
- false
135
+ GitHelper.uncommitted_changes?(brand_path)
153
136
  end
154
137
 
155
138
  # Show file list using git status --short (matches old script)
@@ -768,11 +768,7 @@ module Appydave
768
768
 
769
769
  # Calculate total size of a directory
770
770
  def calculate_directory_size(dir_path)
771
- total = 0
772
- Dir.glob(File.join(dir_path, '**', '*'), File::FNM_DOTMATCH).each do |file|
773
- total += File.size(file) if File.file?(file)
774
- end
775
- total
771
+ FileHelper.calculate_directory_size(dir_path)
776
772
  end
777
773
  end
778
774
  end