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.
- checksums.yaml +4 -4
- data/.rubocop.yml +6 -0
- data/AGENTS.md +22 -0
- data/CHANGELOG.md +12 -0
- data/CLAUDE.md +206 -51
- data/README.md +144 -11
- data/bin/archive_project.rb +249 -0
- data/bin/configuration.rb +21 -1
- data/bin/generate_manifest.rb +357 -0
- data/bin/sync_from_ssd.rb +236 -0
- data/bin/vat +623 -0
- data/docs/README.md +169 -0
- data/docs/configuration/.env.example +19 -0
- data/docs/configuration/README.md +394 -0
- data/docs/configuration/channels.example.json +26 -0
- data/docs/configuration/settings.example.json +6 -0
- data/docs/development/CODEX-recommendations.md +123 -0
- data/docs/development/README.md +100 -0
- data/docs/development/cli-architecture-patterns.md +1604 -0
- data/docs/development/pattern-comparison.md +284 -0
- data/docs/prd-unified-brands-configuration.md +792 -0
- data/docs/project-brand-systems-analysis.md +934 -0
- data/docs/vat/dam-vision.md +123 -0
- data/docs/vat/session-summary-2025-11-09.md +297 -0
- data/docs/vat/usage.md +508 -0
- data/docs/vat/vat-testing-plan.md +801 -0
- data/lib/appydave/tools/configuration/models/brands_config.rb +238 -0
- data/lib/appydave/tools/configuration/models/config_base.rb +7 -0
- data/lib/appydave/tools/configuration/models/settings_config.rb +4 -0
- data/lib/appydave/tools/vat/config.rb +153 -0
- data/lib/appydave/tools/vat/config_loader.rb +91 -0
- data/lib/appydave/tools/vat/manifest_generator.rb +239 -0
- data/lib/appydave/tools/vat/project_listing.rb +198 -0
- data/lib/appydave/tools/vat/project_resolver.rb +132 -0
- data/lib/appydave/tools/vat/s3_operations.rb +560 -0
- data/lib/appydave/tools/version.rb +1 -1
- data/lib/appydave/tools.rb +9 -1
- data/package.json +1 -1
- metadata +57 -3
- 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?
|