appydave-tools 0.16.0 → 0.17.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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +6 -0
  3. data/AGENTS.md +22 -0
  4. data/CHANGELOG.md +12 -0
  5. data/CLAUDE.md +206 -51
  6. data/README.md +144 -11
  7. data/bin/archive_project.rb +249 -0
  8. data/bin/configuration.rb +21 -1
  9. data/bin/generate_manifest.rb +357 -0
  10. data/bin/sync_from_ssd.rb +236 -0
  11. data/bin/vat +623 -0
  12. data/docs/README.md +169 -0
  13. data/docs/configuration/.env.example +19 -0
  14. data/docs/configuration/README.md +394 -0
  15. data/docs/configuration/channels.example.json +26 -0
  16. data/docs/configuration/settings.example.json +6 -0
  17. data/docs/development/CODEX-recommendations.md +123 -0
  18. data/docs/development/README.md +100 -0
  19. data/docs/development/cli-architecture-patterns.md +1604 -0
  20. data/docs/development/pattern-comparison.md +284 -0
  21. data/docs/prd-unified-brands-configuration.md +792 -0
  22. data/docs/project-brand-systems-analysis.md +934 -0
  23. data/docs/vat/dam-vision.md +123 -0
  24. data/docs/vat/session-summary-2025-11-09.md +297 -0
  25. data/docs/vat/usage.md +508 -0
  26. data/docs/vat/vat-testing-plan.md +801 -0
  27. data/lib/appydave/tools/configuration/models/brands_config.rb +238 -0
  28. data/lib/appydave/tools/configuration/models/config_base.rb +7 -0
  29. data/lib/appydave/tools/configuration/models/settings_config.rb +4 -0
  30. data/lib/appydave/tools/vat/config.rb +153 -0
  31. data/lib/appydave/tools/vat/config_loader.rb +91 -0
  32. data/lib/appydave/tools/vat/manifest_generator.rb +239 -0
  33. data/lib/appydave/tools/vat/project_listing.rb +198 -0
  34. data/lib/appydave/tools/vat/project_resolver.rb +132 -0
  35. data/lib/appydave/tools/vat/s3_operations.rb +560 -0
  36. data/lib/appydave/tools/version.rb +1 -1
  37. data/lib/appydave/tools.rb +9 -1
  38. data/package.json +1 -1
  39. metadata +57 -3
  40. data/docs/dam/overview.md +0 -28
@@ -0,0 +1,236 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+ # rubocop:disable all
4
+
5
+ # Sync non-video files from SSD to local video directory
6
+ # Reads projects.json manifest and syncs only projects not in local flat structure
7
+ # Copies transcripts, thumbnails, and documentation while excluding video files
8
+ #
9
+ # Usage: ruby sync_from_ssd.rb [--dry-run]
10
+
11
+ require 'fileutils'
12
+ require 'json'
13
+
14
+ # Load configuration
15
+ SCRIPT_DIR = File.dirname(__FILE__)
16
+ TOOLS_DIR = File.expand_path(File.join(SCRIPT_DIR, '..'))
17
+ require_relative '../lib/config_loader'
18
+
19
+ # Determine paths relative to current working directory (repo root)
20
+ LOCAL_BASE = Dir.pwd
21
+ LOCAL_ARCHIVED = File.join(LOCAL_BASE, 'archived')
22
+ MANIFEST_FILE = File.join(LOCAL_BASE, 'projects.json')
23
+
24
+ # Load SSD_BASE from config
25
+ begin
26
+ config = ConfigLoader.load_from_repo(LOCAL_BASE)
27
+ SSD_BASE = config['SSD_BASE']
28
+ rescue ConfigLoader::ConfigNotFoundError, ConfigLoader::InvalidConfigError => e
29
+ puts e.message
30
+ exit 1
31
+ end
32
+
33
+ # Light file patterns to include (everything except heavy video files)
34
+ LIGHT_FILE_PATTERNS = %w[
35
+ **/*.srt
36
+ **/*.vtt
37
+ **/*.txt
38
+ **/*.md
39
+ **/*.jpg
40
+ **/*.jpeg
41
+ **/*.png
42
+ **/*.webp
43
+ **/*.json
44
+ **/*.yml
45
+ **/*.yaml
46
+ ].freeze
47
+
48
+ # Heavy file patterns to exclude (video files)
49
+ HEAVY_FILE_PATTERNS = %w[
50
+ *.mp4
51
+ *.mov
52
+ *.avi
53
+ *.mkv
54
+ *.webm
55
+ ].freeze
56
+
57
+ def dry_run?
58
+ ARGV.include?('--dry-run')
59
+ end
60
+
61
+ def load_manifest
62
+ unless File.exist?(MANIFEST_FILE)
63
+ puts '❌ projects.json not found!'
64
+ puts ' Run: ruby video-asset-tools/bin/generate_manifest.rb'
65
+ exit 1
66
+ end
67
+
68
+ JSON.parse(File.read(MANIFEST_FILE), symbolize_names: true)
69
+ rescue JSON::ParserError => e
70
+ puts "❌ Error parsing projects.json: #{e.message}"
71
+ exit 1
72
+ end
73
+
74
+ def should_sync_project?(project)
75
+ # Only sync if project exists on SSD but NOT in local flat structure
76
+ return false unless project[:storage][:ssd][:exists]
77
+
78
+ # Skip if exists locally in flat structure
79
+ return false if project[:storage][:local][:exists] && project[:storage][:local][:structure] == 'flat'
80
+
81
+ true
82
+ end
83
+
84
+ def validate_no_flat_conflict(project_id)
85
+ flat_path = File.join(LOCAL_BASE, project_id)
86
+ Dir.exist?(flat_path)
87
+ end
88
+
89
+ def sync_project(project)
90
+ project_id = project[:id]
91
+ ssd_path = File.join(SSD_BASE, project[:storage][:ssd][:path])
92
+ range = project[:storage][:ssd][:path].split('/')[0]
93
+ local_dir = File.join(LOCAL_ARCHIVED, range, project_id)
94
+
95
+ return skip_project_result('SSD path not found') unless Dir.exist?(ssd_path)
96
+ return skip_project_result('Flat folder exists (stale manifest?)') if validate_no_flat_conflict(project_id)
97
+
98
+ prepare_local_directory(local_dir)
99
+ sync_light_files(ssd_path, local_dir)
100
+ end
101
+
102
+ def skip_project_result(reason)
103
+ { skipped: 1, files: 0, bytes: 0, reason: reason }
104
+ end
105
+
106
+ def prepare_local_directory(local_dir)
107
+ FileUtils.mkdir_p(local_dir) if !dry_run? && !Dir.exist?(local_dir)
108
+ end
109
+
110
+ def sync_light_files(ssd_path, local_dir)
111
+ stats = { files: 0, bytes: 0 }
112
+
113
+ LIGHT_FILE_PATTERNS.each do |pattern|
114
+ Dir.glob(File.join(ssd_path, pattern)).each do |source_file|
115
+ next if heavy_file?(source_file)
116
+
117
+ copy_file_stats = copy_light_file(source_file, ssd_path, local_dir)
118
+ stats[:files] += copy_file_stats[:files]
119
+ stats[:bytes] += copy_file_stats[:bytes]
120
+ end
121
+ end
122
+
123
+ stats
124
+ end
125
+
126
+ def heavy_file?(source_file)
127
+ HEAVY_FILE_PATTERNS.any? { |pattern| File.fnmatch(pattern, File.basename(source_file)) }
128
+ end
129
+
130
+ def copy_light_file(source_file, ssd_path, local_dir)
131
+ relative_path = source_file.sub("#{ssd_path}/", '')
132
+ dest_file = File.join(local_dir, relative_path)
133
+
134
+ return { files: 0, bytes: 0 } if file_already_synced?(source_file, dest_file)
135
+
136
+ file_size = File.size(source_file)
137
+ perform_file_copy(source_file, dest_file, relative_path, file_size)
138
+
139
+ { files: 1, bytes: file_size }
140
+ end
141
+
142
+ def file_already_synced?(source_file, dest_file)
143
+ File.exist?(dest_file) && File.size(dest_file) == File.size(source_file)
144
+ end
145
+
146
+ def perform_file_copy(source_file, dest_file, relative_path, file_size)
147
+ if dry_run?
148
+ puts " [DRY-RUN] Would copy: #{relative_path} (#{format_bytes(file_size)})"
149
+ else
150
+ FileUtils.mkdir_p(File.dirname(dest_file))
151
+ FileUtils.cp(source_file, dest_file, preserve: true)
152
+ puts " ✓ Copied: #{relative_path} (#{format_bytes(file_size)})"
153
+ end
154
+ end
155
+
156
+ def format_bytes(bytes)
157
+ if bytes < 1024
158
+ "#{bytes}B"
159
+ elsif bytes < 1024 * 1024
160
+ "#{(bytes / 1024.0).round(1)}KB"
161
+ else
162
+ "#{(bytes / 1024.0 / 1024.0).round(1)}MB"
163
+ end
164
+ end
165
+
166
+ # Main execution
167
+ puts dry_run? ? '🔍 DRY-RUN MODE - No files will be copied' : '📦 Syncing from SSD...'
168
+ puts
169
+
170
+ unless Dir.exist?(SSD_BASE)
171
+ puts "❌ SSD not mounted at #{SSD_BASE}"
172
+ exit 1
173
+ end
174
+
175
+ # Load manifest
176
+ manifest = load_manifest
177
+ puts "📋 Loaded manifest: #{manifest[:projects].size} projects"
178
+ puts " Last updated: #{manifest[:config][:last_updated]}"
179
+ puts
180
+
181
+ # Filter projects to sync
182
+ projects_to_sync = manifest[:projects].select { |p| should_sync_project?(p) }
183
+
184
+ puts '🔍 Analysis:'
185
+ puts " Total projects in manifest: #{manifest[:projects].size}"
186
+ puts " Projects to sync: #{projects_to_sync.size}"
187
+ puts " Skipped (in flat structure): #{manifest[:projects].size - projects_to_sync.size}"
188
+ puts
189
+
190
+ if projects_to_sync.empty?
191
+ puts '✅ Nothing to sync - all projects either in flat structure or already synced'
192
+ exit 0
193
+ end
194
+
195
+ total_stats = { files: 0, bytes: 0, skipped: 0, validation_skipped: 0 }
196
+
197
+ projects_to_sync.each do |project|
198
+ stats = sync_project(project)
199
+
200
+ # Only show project if there are files to sync or a warning
201
+ if stats[:reason] || stats[:files]&.positive?
202
+ puts "📁 #{project[:id]}"
203
+
204
+ if stats[:reason]
205
+ puts " ⚠️ Skipped: #{stats[:reason]}"
206
+ total_stats[:validation_skipped] += 1 if stats[:reason].include?('stale')
207
+ end
208
+
209
+ puts " #{stats[:files]} file(s), #{format_bytes(stats[:bytes])}" if stats[:files]&.positive?
210
+ puts
211
+ end
212
+
213
+ total_stats[:files] += stats[:files] || 0
214
+ total_stats[:bytes] += stats[:bytes] || 0
215
+ total_stats[:skipped] += stats[:skipped] || 0
216
+ end
217
+
218
+ puts
219
+ puts '=' * 60
220
+ puts 'Summary:'
221
+ puts " Projects scanned: #{projects_to_sync.size}"
222
+ puts " Projects skipped (validation): #{total_stats[:validation_skipped]}" if total_stats[:validation_skipped].positive?
223
+ puts " Files #{dry_run? ? 'to copy' : 'copied'}: #{total_stats[:files]}"
224
+ puts " Total size: #{format_bytes(total_stats[:bytes])}"
225
+ puts
226
+
227
+ if total_stats[:validation_skipped].positive?
228
+ puts '⚠️ WARNING: Some projects were skipped due to validation failures'
229
+ puts ' This may indicate a stale manifest. Consider running:'
230
+ puts ' ruby video-asset-tools/bin/generate_manifest.rb'
231
+ puts
232
+ end
233
+
234
+ puts '✅ Sync complete!'
235
+ puts ' Run without --dry-run to perform the sync' if dry_run?
236
+ puts " Run 'ruby video-asset-tools/bin/generate_manifest.rb' to update manifest with new state" unless dry_run?