appydave-tools 0.17.0 → 0.18.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,241 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'json'
5
+
6
+ module Appydave
7
+ module Tools
8
+ module Dam
9
+ # Sync light files from SSD to local for a brand
10
+ # Only copies non-video files (subtitles, images, docs)
11
+ class SyncFromSsd
12
+ attr_reader :brand, :brand_info, :brand_path
13
+
14
+ # Light file patterns to include (everything except heavy video files)
15
+ LIGHT_FILE_PATTERNS = %w[
16
+ **/*.srt
17
+ **/*.vtt
18
+ **/*.txt
19
+ **/*.md
20
+ **/*.jpg
21
+ **/*.jpeg
22
+ **/*.png
23
+ **/*.webp
24
+ **/*.json
25
+ **/*.yml
26
+ **/*.yaml
27
+ ].freeze
28
+
29
+ # Heavy file patterns to exclude (video files)
30
+ HEAVY_FILE_PATTERNS = %w[
31
+ *.mp4
32
+ *.mov
33
+ *.avi
34
+ *.mkv
35
+ *.webm
36
+ ].freeze
37
+
38
+ def initialize(brand, brand_info: nil, brand_path: nil)
39
+ @brand_info = brand_info || load_brand_info(brand)
40
+ @brand = @brand_info.key # Use resolved brand key, not original input
41
+ @brand_path = brand_path || Config.brand_path(@brand)
42
+ end
43
+
44
+ # Sync light files from SSD for all projects in manifest
45
+ def sync(dry_run: false)
46
+ puts dry_run ? '🔍 DRY-RUN MODE - No files will be copied' : '📦 Syncing from SSD...'
47
+ puts ''
48
+
49
+ # Validate SSD is mounted
50
+ ssd_backup = brand_info.locations.ssd_backup
51
+ unless ssd_backup && !ssd_backup.empty?
52
+ puts "❌ SSD backup location not configured for brand '#{brand}'"
53
+ return
54
+ end
55
+
56
+ unless Dir.exist?(ssd_backup)
57
+ puts "❌ SSD not mounted at #{ssd_backup}"
58
+ return
59
+ end
60
+
61
+ # Load manifest
62
+ manifest_file = File.join(brand_path, 'projects.json')
63
+ unless File.exist?(manifest_file)
64
+ puts '❌ projects.json not found!'
65
+ puts " Run: vat manifest #{brand}"
66
+ return
67
+ end
68
+
69
+ manifest = load_manifest(manifest_file)
70
+ puts "📋 Loaded manifest: #{manifest[:projects].size} projects"
71
+ puts " Last updated: #{manifest[:config][:last_updated]}"
72
+ puts ''
73
+
74
+ # Filter projects to sync
75
+ projects_to_sync = manifest[:projects].select { |p| should_sync_project?(p) }
76
+
77
+ puts '🔍 Analysis:'
78
+ puts " Total projects in manifest: #{manifest[:projects].size}"
79
+ puts " Projects to sync: #{projects_to_sync.size}"
80
+ puts " Skipped (already local): #{manifest[:projects].size - projects_to_sync.size}"
81
+ puts ''
82
+
83
+ if projects_to_sync.empty?
84
+ puts '✅ Nothing to sync - all projects either already local or not on SSD'
85
+ return
86
+ end
87
+
88
+ # Sync each project
89
+ total_stats = { files: 0, bytes: 0, skipped: 0 }
90
+
91
+ projects_to_sync.each do |project|
92
+ stats = sync_project(project, ssd_backup, dry_run: dry_run)
93
+
94
+ # Only show project if there are files to sync or a warning
95
+ if stats[:reason] || stats[:files]&.positive?
96
+ puts "📁 #{project[:id]}"
97
+
98
+ puts " ⚠️ Skipped: #{stats[:reason]}" if stats[:reason]
99
+
100
+ puts " #{stats[:files]} file(s), #{format_bytes(stats[:bytes])}" if stats[:files]&.positive?
101
+ puts ''
102
+ end
103
+
104
+ total_stats[:files] += stats[:files] || 0
105
+ total_stats[:bytes] += stats[:bytes] || 0
106
+ total_stats[:skipped] += stats[:skipped] || 0
107
+ end
108
+
109
+ puts ''
110
+ puts '=' * 60
111
+ puts 'Summary:'
112
+ puts " Projects scanned: #{projects_to_sync.size}"
113
+ puts " Files #{dry_run ? 'to copy' : 'copied'}: #{total_stats[:files]}"
114
+ puts " Total size: #{format_bytes(total_stats[:bytes])}"
115
+ puts ''
116
+ puts '✅ Sync complete!'
117
+ puts ' Run without --dry-run to perform the sync' if dry_run
118
+ end
119
+
120
+ private
121
+
122
+ def load_brand_info(brand)
123
+ Appydave::Tools::Configuration::Config.configure
124
+ Appydave::Tools::Configuration::Config.brands.get_brand(brand)
125
+ end
126
+
127
+ def load_manifest(manifest_file)
128
+ JSON.parse(File.read(manifest_file), symbolize_names: true)
129
+ rescue JSON::ParserError => e
130
+ puts "❌ Error parsing projects.json: #{e.message}"
131
+ exit 1
132
+ end
133
+
134
+ # Determine if project should be synced
135
+ def should_sync_project?(project)
136
+ # Only sync if project exists on SSD but NOT in local flat structure
137
+ return false unless project[:storage][:ssd][:exists]
138
+
139
+ # Skip if exists locally in flat structure
140
+ return false if project[:storage][:local][:exists] && project[:storage][:local][:structure] == 'flat'
141
+
142
+ true
143
+ end
144
+
145
+ # Sync a single project from SSD to local
146
+ def sync_project(project, ssd_backup, dry_run: false)
147
+ project_id = project[:id]
148
+ ssd_path = File.join(ssd_backup, project_id)
149
+
150
+ return { skipped: 1, files: 0, bytes: 0, reason: 'SSD path not found' } unless Dir.exist?(ssd_path)
151
+
152
+ # Check for flat folder conflict (stale manifest)
153
+ flat_path = File.join(brand_path, project_id)
154
+ return { skipped: 1, files: 0, bytes: 0, reason: 'Flat folder exists (stale manifest?)' } if Dir.exist?(flat_path)
155
+
156
+ # Determine local destination path (archived structure)
157
+ # Extract range from project ID (e.g., b65 → 60-69 range)
158
+ range = determine_range(project_id)
159
+ local_dir = File.join(brand_path, 'archived', range, project_id)
160
+
161
+ # Create local directory
162
+ FileUtils.mkdir_p(local_dir) if !dry_run && !Dir.exist?(local_dir)
163
+
164
+ # Sync light files
165
+ sync_light_files(ssd_path, local_dir, dry_run: dry_run)
166
+ end
167
+
168
+ # Determine range folder for project (e.g., b65 → 60-69)
169
+ def determine_range(project_id)
170
+ # FliVideo pattern: b40, b41, ... b99
171
+ if project_id =~ /^b(\d+)/
172
+ tens = (Regexp.last_match(1).to_i / 10) * 10
173
+ "#{tens}-#{tens + 9}"
174
+ else
175
+ # Legacy pattern or unknown: use first 3 chars
176
+ '000-099'
177
+ end
178
+ end
179
+
180
+ # Sync light files from SSD to local
181
+ def sync_light_files(ssd_path, local_dir, dry_run: false)
182
+ stats = { files: 0, bytes: 0 }
183
+
184
+ LIGHT_FILE_PATTERNS.each do |pattern|
185
+ Dir.glob(File.join(ssd_path, pattern)).each do |source_file|
186
+ next if heavy_file?(source_file)
187
+
188
+ copy_stats = copy_light_file(source_file, ssd_path, local_dir, dry_run: dry_run)
189
+ stats[:files] += copy_stats[:files]
190
+ stats[:bytes] += copy_stats[:bytes]
191
+ end
192
+ end
193
+
194
+ stats
195
+ end
196
+
197
+ # Check if file is a heavy video file
198
+ def heavy_file?(source_file)
199
+ HEAVY_FILE_PATTERNS.any? { |pattern| File.fnmatch(pattern, File.basename(source_file)) }
200
+ end
201
+
202
+ # Copy a single light file
203
+ def copy_light_file(source_file, ssd_path, local_dir, dry_run: false)
204
+ relative_path = source_file.sub("#{ssd_path}/", '')
205
+ dest_file = File.join(local_dir, relative_path)
206
+
207
+ # Skip if already synced (same size)
208
+ return { files: 0, bytes: 0 } if file_already_synced?(source_file, dest_file)
209
+
210
+ file_size = File.size(source_file)
211
+
212
+ if dry_run
213
+ puts " [DRY-RUN] Would copy: #{relative_path} (#{format_bytes(file_size)})"
214
+ else
215
+ FileUtils.mkdir_p(File.dirname(dest_file))
216
+ FileUtils.cp(source_file, dest_file, preserve: true)
217
+ puts " ✓ Copied: #{relative_path} (#{format_bytes(file_size)})"
218
+ end
219
+
220
+ { files: 1, bytes: file_size }
221
+ end
222
+
223
+ # Check if file is already synced (by size comparison)
224
+ def file_already_synced?(source_file, dest_file)
225
+ File.exist?(dest_file) && File.size(dest_file) == File.size(source_file)
226
+ end
227
+
228
+ # Format bytes into human-readable format
229
+ def format_bytes(bytes)
230
+ if bytes < 1024
231
+ "#{bytes}B"
232
+ elsif bytes < 1024 * 1024
233
+ "#{(bytes / 1024.0).round(1)}KB"
234
+ else
235
+ "#{(bytes / 1024.0 / 1024.0).round(1)}MB"
236
+ end
237
+ end
238
+ end
239
+ end
240
+ end
241
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Appydave
4
4
  module Tools
5
- VERSION = '0.17.0'
5
+ VERSION = '0.18.0'
6
6
  end
7
7
  end
@@ -52,12 +52,13 @@ require 'appydave/tools/prompt_tools/prompt_completion'
52
52
  require 'appydave/tools/subtitle_processor/clean'
53
53
  require 'appydave/tools/subtitle_processor/join'
54
54
 
55
- require 'appydave/tools/vat/config'
56
- require 'appydave/tools/vat/project_resolver'
57
- require 'appydave/tools/vat/config_loader'
58
- require 'appydave/tools/vat/s3_operations'
59
- require 'appydave/tools/vat/project_listing'
60
- require 'appydave/tools/vat/manifest_generator'
55
+ require 'appydave/tools/dam/config'
56
+ require 'appydave/tools/dam/project_resolver'
57
+ require 'appydave/tools/dam/config_loader'
58
+ require 'appydave/tools/dam/s3_operations'
59
+ require 'appydave/tools/dam/project_listing'
60
+ require 'appydave/tools/dam/manifest_generator'
61
+ require 'appydave/tools/dam/sync_from_ssd'
61
62
 
62
63
  require 'appydave/tools/youtube_automation/gpt_agent'
63
64
 
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appydave-tools",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "AppyDave YouTube Automation Tools",
5
5
  "scripts": {
6
6
  "release": "semantic-release"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: appydave-tools
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.17.0
4
+ version: 0.18.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Cruwys
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-11-09 00:00:00.000000000 Z
11
+ date: 2025-11-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -214,6 +214,7 @@ files:
214
214
  - bin/bank_reconciliation.rb
215
215
  - bin/configuration.rb
216
216
  - bin/console
217
+ - bin/dam
217
218
  - bin/generate_manifest.rb
218
219
  - bin/gpt_context.rb
219
220
  - bin/move_images.rb
@@ -223,7 +224,6 @@ files:
223
224
  - bin/subtitle_manager.rb
224
225
  - bin/subtitle_processor.rb
225
226
  - bin/sync_from_ssd.rb
226
- - bin/vat
227
227
  - bin/youtube_automation.rb
228
228
  - bin/youtube_manager.rb
229
229
  - docs/README.md
@@ -237,6 +237,10 @@ files:
237
237
  - docs/configuration/README.md
238
238
  - docs/configuration/channels.example.json
239
239
  - docs/configuration/settings.example.json
240
+ - docs/dam/dam-vision.md
241
+ - docs/dam/session-summary-2025-11-09.md
242
+ - docs/dam/usage.md
243
+ - docs/dam/vat-testing-plan.md
240
244
  - docs/development/CODEX-recommendations.md
241
245
  - docs/development/README.md
242
246
  - docs/development/cli-architecture-patterns.md
@@ -254,10 +258,6 @@ files:
254
258
  - docs/tools/subtitle-processor.md
255
259
  - docs/tools/youtube-automation.md
256
260
  - docs/tools/youtube-manager.md
257
- - docs/vat/dam-vision.md
258
- - docs/vat/session-summary-2025-11-09.md
259
- - docs/vat/usage.md
260
- - docs/vat/vat-testing-plan.md
261
261
  - exe/ad_config
262
262
  - exe/gpt_context
263
263
  - exe/prompt_tools
@@ -281,6 +281,13 @@ files:
281
281
  - lib/appydave/tools/configuration/models/settings_config.rb
282
282
  - lib/appydave/tools/configuration/models/youtube_automation_config.rb
283
283
  - lib/appydave/tools/configuration/openai.rb
284
+ - lib/appydave/tools/dam/config.rb
285
+ - lib/appydave/tools/dam/config_loader.rb
286
+ - lib/appydave/tools/dam/manifest_generator.rb
287
+ - lib/appydave/tools/dam/project_listing.rb
288
+ - lib/appydave/tools/dam/project_resolver.rb
289
+ - lib/appydave/tools/dam/s3_operations.rb
290
+ - lib/appydave/tools/dam/sync_from_ssd.rb
284
291
  - lib/appydave/tools/debuggable.rb
285
292
  - lib/appydave/tools/gpt_context/_doc.md
286
293
  - lib/appydave/tools/gpt_context/file_collector.rb
@@ -301,12 +308,6 @@ files:
301
308
  - lib/appydave/tools/types/base_model.rb
302
309
  - lib/appydave/tools/types/hash_type.rb
303
310
  - lib/appydave/tools/types/indifferent_access_hash.rb
304
- - lib/appydave/tools/vat/config.rb
305
- - lib/appydave/tools/vat/config_loader.rb
306
- - lib/appydave/tools/vat/manifest_generator.rb
307
- - lib/appydave/tools/vat/project_listing.rb
308
- - lib/appydave/tools/vat/project_resolver.rb
309
- - lib/appydave/tools/vat/s3_operations.rb
310
311
  - lib/appydave/tools/version.rb
311
312
  - lib/appydave/tools/youtube_automation/_doc.md
312
313
  - lib/appydave/tools/youtube_automation/gpt_agent.rb
@@ -1,297 +0,0 @@
1
- # VAT S3 Operations - Session Summary
2
- **Date**: 2025-11-09
3
- **Status**: Ready to Continue - Archive/SSD Commands Next
4
-
5
- ---
6
-
7
- ## What We Accomplished Today
8
-
9
- ### 1. Fixed RuboCop Issues Properly ✅
10
-
11
- **Problem**: Initially disabled RuboCop cops instead of fixing underlying issues.
12
-
13
- **Solution**: Refactored S3Operations to use **dependency injection**:
14
- ```ruby
15
- # Before (mocking singletons):
16
- allow_any_instance_of(BrandsConfig).to receive(:get_brand)
17
- S3Operations.new('test', 'test-project')
18
-
19
- # After (dependency injection):
20
- S3Operations.new('test', 'test-project',
21
- brand_info: real_brand_info_object,
22
- brand_path: brand_path,
23
- s3_client: mock_s3_client
24
- )
25
- ```
26
-
27
- **Benefits**:
28
- - Eliminated ALL `allow_any_instance_of` usage
29
- - Tests use real `BrandInfo` objects (not mocks)
30
- - More flexible, testable, SOLID design
31
- - Backward compatible (production code unchanged)
32
-
33
- **RuboCop Config**: Disabled `RSpec/MessageSpies` and `RSpec/StubbedMock` globally (legitimate for AWS SDK integration tests)
34
-
35
- ### 2. Added S3 Cleanup Local Command ✅
36
-
37
- **New Command**: `vat s3-cleanup-local <brand> <project> --force [--dry-run]`
38
-
39
- **Features**:
40
- - Deletes files from local `s3-staging/` directory
41
- - Requires `--force` flag for safety
42
- - Supports `--dry-run` mode
43
- - Removes empty directories after cleanup
44
- - Shows deleted/failed counts
45
- - Auto-detection from PWD
46
-
47
- **Implementation**:
48
- - Added `cleanup_local` method to S3Operations (lib/appydave/tools/vat/s3_operations.rb:212-275)
49
- - Added `delete_local_file` private method (lib/appydave/tools/vat/s3_operations.rb:359-371)
50
- - Added CLI command handler
51
- - Added comprehensive help documentation
52
-
53
- **Tests**: 6 tests covering all scenarios (289 total tests, 90.59% coverage)
54
-
55
- ### 3. Fixed S3 Status Command ✅
56
-
57
- **Problem**: Status only showed 3 states (synced, modified, S3 only) - missing "local only" files.
58
-
59
- **Solution**: Enhanced status to show all 4 states:
60
- - ✓ **[synced]** - Files match (MD5)
61
- - ⚠️ **[modified]** - Files differ
62
- - ☁️ **[S3 only]** - File in S3 but not local
63
- - 📁 **[local only]** - File local but not in S3 ⭐ NEW
64
-
65
- **Implementation**:
66
- - Rewrote `status` method to check both S3 AND local files (lib/appydave/tools/vat/s3_operations.rb:139-202)
67
- - Added `list_local_files` helper method (lib/appydave/tools/vat/s3_operations.rb:424-434)
68
- - Enhanced summary: Shows file counts and sizes for both S3 and local
69
-
70
- **Tests**: Added 2 new tests (local-only files, comprehensive summary)
71
-
72
- ### 4. Renamed Cleanup Commands for Consistency ✅
73
-
74
- **Old Names**:
75
- - `vat s3-cleanup` → Delete S3 files
76
- - `vat cleanup-local` → Delete local files
77
-
78
- **New Names**:
79
- - `vat s3-cleanup-remote` → Delete S3 files
80
- - `vat s3-cleanup-local` → Delete local files
81
-
82
- **Backward Compatibility**: Old names still work but show deprecation warning
83
-
84
- **Changes**:
85
- - Updated command registration (bin/vat:13-24)
86
- - Renamed methods
87
- - Updated all help documentation
88
- - Added deprecation notices
89
-
90
- ---
91
-
92
- ## Testing Status
93
-
94
- ### Automated Tests ✅
95
- - **Session 1**: 289 examples, 0 failures, 90.59% coverage
96
- - **Session 2**: 297 examples, 0 failures, 90.69% coverage (+8 tests, +0.10% coverage)
97
- - **RuboCop**: Clean (no offenses)
98
-
99
- ### Manual Testing (David) ✅
100
- **Session 1**:
101
- - ✅ `vat s3-cleanup-remote` - Tested and working
102
- - ✅ `vat s3-cleanup-local` - Tested and working
103
- - ✅ `vat s3-status` - Shows all 4 states correctly (synced, modified, S3 only, local only)
104
-
105
- **Session 2**:
106
- - ⏳ `vat archive` - Ready for manual testing
107
-
108
- ### S3Operations Test Coverage
109
- - **Session 1**: 33 tests (upload, download, status, cleanup, cleanup_local)
110
- - **Session 2**: 41 tests (+8 archive tests)
111
-
112
- ---
113
-
114
- ## Current State
115
-
116
- ### Implemented VAT Commands
117
- 1. ✅ `vat list [brand] [pattern]` - List brands/projects
118
- 2. ✅ `vat s3-up <brand> <project>` - Upload to S3
119
- 3. ✅ `vat s3-down <brand> <project>` - Download from S3
120
- 4. ✅ `vat s3-status <brand> <project>` - Check sync status (all 4 states)
121
- 5. ✅ `vat s3-cleanup-remote <brand> <project>` - Delete S3 files
122
- 6. ✅ `vat s3-cleanup-local <brand> <project>` - Delete local files
123
- 7. ✅ `vat archive <brand> <project>` - Copy to SSD backup ⭐ NEW (2025-11-09 Session 2)
124
-
125
- ### Not Yet Implemented
126
- 1. ⏳ `vat sync-ssd <brand>` - Restore from SSD
127
-
128
- ---
129
-
130
- ## Session 2 (2025-11-09 Evening) - Archive Command Implementation ✅
131
-
132
- ### What We Accomplished
133
-
134
- #### 1. Implemented Archive Command ✅
135
- **New Command**: `vat archive <brand> <project> [--force] [--dry-run]`
136
-
137
- **Features**:
138
- - Copies entire project directory to SSD backup location
139
- - Verifies SSD is mounted before archiving
140
- - Shows project size before copying
141
- - Optional: Delete local copy after successful archive (--force)
142
- - Dry-run support for preview
143
- - Auto-detection from PWD
144
-
145
- **Implementation**:
146
- - Added `archive` method to S3Operations (lib/appydave/tools/vat/s3_operations.rb:298-340)
147
- - Added helper methods:
148
- - `copy_to_ssd` (lib/appydave/tools/vat/s3_operations.rb:493-522)
149
- - `delete_local_project` (lib/appydave/tools/vat/s3_operations.rb:524-547)
150
- - `calculate_directory_size` (lib/appydave/tools/vat/s3_operations.rb:549-556)
151
- - Added CLI command handler (bin/vat:148-156)
152
- - Added comprehensive help documentation (bin/vat:480-515)
153
-
154
- **Tests**: 8 new tests covering all scenarios (297 total tests, 90.69% coverage)
155
-
156
- **Test Coverage**:
157
- - ✅ SSD backup location not configured
158
- - ✅ SSD not mounted
159
- - ✅ Project does not exist
160
- - ✅ Dry-run preview
161
- - ✅ Copy without deleting local (default)
162
- - ✅ Warning when not using --force
163
- - ✅ Copy and delete with --force
164
- - ✅ Skip when already exists on SSD
165
-
166
- **Storage Strategy**: Local → S3 (90-day collaboration) → SSD (long-term archive)
167
-
168
- **Configuration**: Uses `ssd_backup` location from brands.json (already configured for all 6 brands)
169
-
170
- ---
171
-
172
- ## Next Steps (For Future Session)
173
-
174
- ### Priority 1: Sync from SSD Command ⏳
175
- Implement `vat sync-ssd <brand>` to restore projects from SSD:
176
-
177
- **Requirements** (from original discussion):
178
- - Copy entire project directory to SSD backup location
179
- - Verify copy completed successfully
180
- - Option to remove local copy after successful archive
181
- - Support dry-run mode
182
- - Show progress for large projects
183
-
184
- **Configuration** (already in brands.json):
185
- ```json
186
- "locations": {
187
- "video_projects": "/Users/davidcruwys/dev/video-projects/v-appydave",
188
- "ssd_backup": "/Volumes/T7/youtube-PUBLISHED/appydave"
189
- }
190
- ```
191
-
192
- **Implementation Plan**:
193
- 1. Add `archive` method to new class (or S3Operations? Or separate ArchiveOperations?)
194
- 2. CLI command handler
195
- 3. Tests
196
- 4. Help documentation
197
-
198
- ### Priority 2: Sync from SSD Command
199
- Implement `vat sync-ssd <brand>` to restore projects from SSD:
200
-
201
- **Requirements**:
202
- - List available projects on SSD
203
- - Copy selected project(s) back to local
204
- - Smart sync (skip if already exists? overwrite? merge?)
205
- - Dry-run support
206
-
207
- ### Priority 3: Documentation
208
- - Document AWS permissions strategy for team members
209
- - Update CLAUDE.md with latest VAT commands
210
- - Update usage documentation
211
-
212
- ---
213
-
214
- ## Architecture Notes
215
-
216
- ### Dependency Injection Pattern Established
217
- All new code should follow the pattern established in S3Operations:
218
-
219
- ```ruby
220
- class MyOperations
221
- def initialize(brand, project_id, brand_info: nil, brand_path: nil)
222
- @brand = brand
223
- @project_id = project_id
224
- @brand_info = brand_info || load_from_config(brand)
225
- @brand_path = brand_path || Config.brand_path(brand)
226
- end
227
-
228
- private
229
-
230
- def load_from_config(brand)
231
- Config.configure
232
- Config.brands.get_brand(brand)
233
- end
234
- end
235
- ```
236
-
237
- **Tests**:
238
- ```ruby
239
- let(:brand_info) { BrandInfo.new('test', test_data) } # Real object
240
-
241
- def create_operations
242
- described_class.new('test', 'test-project',
243
- brand_info: brand_info,
244
- brand_path: brand_path
245
- )
246
- end
247
- ```
248
-
249
- ### File Locations
250
- - **S3 operations**: `lib/appydave/tools/vat/s3_operations.rb`
251
- - **S3 tests**: `spec/appydave/tools/vat/s3_operations_spec.rb`
252
- - **VAT CLI**: `bin/vat`
253
- - **Brands config**: `lib/appydave/tools/configuration/models/brands_config.rb`
254
-
255
- ---
256
-
257
- ## Questions for Tomorrow
258
-
259
- 1. **Archive behavior**: Should archive DELETE local files after successful copy, or just copy and leave local intact?
260
- 2. **Archive scope**: Archive just the project directory, or also s3-staging/?
261
- 3. **SSD sync**: Should it sync ALL projects from a brand, or let user select specific ones?
262
- 4. **Conflict handling**: If project exists both locally and on SSD, what should sync-ssd do?
263
-
264
- ---
265
-
266
- ## Key Files Modified
267
-
268
- ### Session 1
269
- 1. `lib/appydave/tools/vat/s3_operations.rb` - Refactored with DI, added cleanup_local, fixed status
270
- 2. `spec/appydave/tools/vat/s3_operations_spec.rb` - Rewrote to use DI, added new tests
271
- 3. `bin/vat` - Renamed cleanup commands, updated help
272
- 4. `.rubocop.yml` - Disabled MessageSpies and StubbedMock cops
273
-
274
- ### Session 2
275
- 1. `lib/appydave/tools/vat/s3_operations.rb` - Added archive method and helper methods
276
- 2. `spec/appydave/tools/vat/s3_operations_spec.rb` - Added 8 archive tests
277
- 3. `bin/vat` - Added archive command handler and help documentation
278
- 4. `docs/vat/session-summary-2025-11-09.md` - Updated with Session 2 accomplishments
279
-
280
- ---
281
-
282
- ## Git Status
283
- **Session 1**:
284
- - 289 tests passing
285
- - RuboCop clean
286
- - S3 cleanup commands complete
287
-
288
- **Session 2**:
289
- - 297 tests passing (+8 archive tests)
290
- - 90.69% code coverage (+0.10%)
291
- - RuboCop clean
292
- - Archive command complete
293
- - Ready to commit
294
-
295
- ---
296
-
297
- **Next Session**: Implement `vat sync-ssd <brand>` command for restoring projects from SSD backup.