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,238 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appydave
4
+ module Tools
5
+ module Configuration
6
+ module Models
7
+ # Brands configuration for video project management
8
+ class BrandsConfig < ConfigBase
9
+ # Retrieve brand information by brand key (string or symbol)
10
+ def get_brand(brand_key)
11
+ brand_key = brand_key.to_s
12
+ info = data['brands'][brand_key] || default_brand_info
13
+ BrandInfo.new(brand_key, info)
14
+ end
15
+
16
+ # Set brand information
17
+ def set_brand(brand_key, brand_info)
18
+ data['brands'] ||= {}
19
+ data['brands'][brand_key.to_s] = brand_info.to_h
20
+ end
21
+
22
+ # Retrieve a list of all brands
23
+ def brands
24
+ data['brands'].map do |key, info|
25
+ BrandInfo.new(key, info)
26
+ end
27
+ end
28
+
29
+ # Get brands for a specific user
30
+ def get_brands_for_user(user_key)
31
+ user_key = user_key.to_s
32
+ brands.select { |brand| brand.team.include?(user_key) }
33
+ end
34
+
35
+ # Get user information
36
+ def get_user(user_key)
37
+ user_key = user_key.to_s
38
+ info = data['users'][user_key] || default_user_info
39
+ UserInfo.new(user_key, info)
40
+ end
41
+
42
+ # Set user information
43
+ def set_user(user_key, user_info)
44
+ data['users'] ||= {}
45
+ data['users'][user_key.to_s] = user_info.to_h
46
+ end
47
+
48
+ # Retrieve a list of all users
49
+ def users
50
+ data['users'].map do |key, info|
51
+ UserInfo.new(key, info)
52
+ end
53
+ end
54
+
55
+ def key?(key)
56
+ key = key.to_s
57
+ data['brands'].key?(key)
58
+ end
59
+
60
+ def shortcut?(shortcut)
61
+ shortcut = shortcut.to_s
62
+ data['brands'].values.any? { |info| info['shortcut'] == shortcut }
63
+ end
64
+
65
+ def print
66
+ log.heading 'Brands Configuration'
67
+
68
+ print_brands = brands.map do |brand|
69
+ {
70
+ key: brand.key,
71
+ name: brand.name,
72
+ shortcut: brand.shortcut,
73
+ type: brand.type,
74
+ youtube_channels: brand.youtube_channels.join(', '),
75
+ team: brand.team.join(', '),
76
+ video_projects: print_location(brand.locations.video_projects),
77
+ ssd_backup: print_location(brand.locations.ssd_backup),
78
+ aws_profile: brand.aws.profile
79
+ }
80
+ end
81
+
82
+ tp print_brands, :key, :name, :shortcut, :type, :youtube_channels, :team, :video_projects, :ssd_backup, :aws_profile
83
+ end
84
+
85
+ private
86
+
87
+ def print_location(location)
88
+ return 'Not Set' unless location
89
+ return 'Not Set' if location.empty?
90
+
91
+ File.exist?(location) ? 'TRUE' : 'false'
92
+ end
93
+
94
+ def default_data
95
+ { 'brands' => {}, 'users' => {} }
96
+ end
97
+
98
+ def default_brand_info
99
+ {
100
+ 'name' => '',
101
+ 'shortcut' => '',
102
+ 'type' => 'owned',
103
+ 'youtube_channels' => [],
104
+ 'team' => [],
105
+ 'locations' => {
106
+ 'video_projects' => '',
107
+ 'ssd_backup' => ''
108
+ },
109
+ 'aws' => {
110
+ 'profile' => '',
111
+ 'region' => 'ap-southeast-1',
112
+ 's3_bucket' => '',
113
+ 's3_prefix' => ''
114
+ },
115
+ 'settings' => {
116
+ 's3_cleanup_days' => 90
117
+ }
118
+ }
119
+ end
120
+
121
+ def default_user_info
122
+ {
123
+ 'name' => '',
124
+ 'email' => '',
125
+ 'role' => 'team_member',
126
+ 'default_aws_profile' => ''
127
+ }
128
+ end
129
+
130
+ # Type-safe class to access brand properties
131
+ class BrandInfo
132
+ attr_accessor :key, :name, :shortcut, :type, :youtube_channels, :team, :locations, :aws, :settings
133
+
134
+ def initialize(key, data)
135
+ @key = key
136
+ @name = data['name']
137
+ @shortcut = data['shortcut']
138
+ @type = data['type'] || 'owned'
139
+ @youtube_channels = data['youtube_channels'] || []
140
+ @team = data['team'] || []
141
+ @locations = BrandLocation.new(data['locations'] || {})
142
+ @aws = BrandAws.new(data['aws'] || {})
143
+ @settings = BrandSettings.new(data['settings'] || {})
144
+ end
145
+
146
+ def to_h
147
+ {
148
+ 'name' => @name,
149
+ 'shortcut' => @shortcut,
150
+ 'type' => @type,
151
+ 'youtube_channels' => @youtube_channels,
152
+ 'team' => @team,
153
+ 'locations' => @locations.to_h,
154
+ 'aws' => @aws.to_h,
155
+ 'settings' => @settings.to_h
156
+ }
157
+ end
158
+ end
159
+
160
+ # Type-safe class to access brand location properties
161
+ class BrandLocation
162
+ attr_accessor :video_projects, :ssd_backup
163
+
164
+ def initialize(data)
165
+ @video_projects = data['video_projects']
166
+ @ssd_backup = data['ssd_backup']
167
+ end
168
+
169
+ def to_h
170
+ {
171
+ 'video_projects' => @video_projects,
172
+ 'ssd_backup' => @ssd_backup
173
+ }
174
+ end
175
+ end
176
+
177
+ # Type-safe class to access brand AWS properties
178
+ class BrandAws
179
+ attr_accessor :profile, :region, :s3_bucket, :s3_prefix
180
+
181
+ def initialize(data)
182
+ @profile = data['profile']
183
+ @region = data['region'] || 'ap-southeast-1'
184
+ @s3_bucket = data['s3_bucket']
185
+ @s3_prefix = data['s3_prefix']
186
+ end
187
+
188
+ def to_h
189
+ {
190
+ 'profile' => @profile,
191
+ 'region' => @region,
192
+ 's3_bucket' => @s3_bucket,
193
+ 's3_prefix' => @s3_prefix
194
+ }
195
+ end
196
+ end
197
+
198
+ # Type-safe class to access brand settings
199
+ class BrandSettings
200
+ attr_accessor :s3_cleanup_days
201
+
202
+ def initialize(data)
203
+ @s3_cleanup_days = data['s3_cleanup_days'] || 90
204
+ end
205
+
206
+ def to_h
207
+ {
208
+ 's3_cleanup_days' => @s3_cleanup_days
209
+ }
210
+ end
211
+ end
212
+
213
+ # Type-safe class to access user properties
214
+ class UserInfo
215
+ attr_accessor :key, :name, :email, :role, :default_aws_profile
216
+
217
+ def initialize(key, data)
218
+ @key = key
219
+ @name = data['name']
220
+ @email = data['email']
221
+ @role = data['role'] || 'team_member'
222
+ @default_aws_profile = data['default_aws_profile']
223
+ end
224
+
225
+ def to_h
226
+ {
227
+ 'name' => @name,
228
+ 'email' => @email,
229
+ 'role' => @role,
230
+ 'default_aws_profile' => @default_aws_profile
231
+ }
232
+ end
233
+ end
234
+ end
235
+ end
236
+ end
237
+ end
238
+ end
@@ -20,6 +20,13 @@ module Appydave
20
20
  end
21
21
 
22
22
  def save
23
+ # Create backup if file exists
24
+ if File.exist?(config_path)
25
+ backup_path = "#{config_path}.backup.#{Time.now.strftime('%Y%m%d-%H%M%S')}"
26
+ FileUtils.cp(config_path, backup_path)
27
+ log.info "Backup created: #{backup_path}" if respond_to?(:log)
28
+ end
29
+
23
30
  File.write(config_path, JSON.pretty_generate(data))
24
31
  end
25
32
 
@@ -16,6 +16,10 @@ module Appydave
16
16
 
17
17
  # Well known settings
18
18
 
19
+ def video_projects_root
20
+ get('video-projects-root')
21
+ end
22
+
19
23
  def ecamm_recording_folder
20
24
  get('ecamm-recording-folder')
21
25
  end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appydave
4
+ module Tools
5
+ module Vat
6
+ # VatConfig - Configuration management for Video Asset Tools
7
+ #
8
+ # Manages VIDEO_PROJECTS_ROOT and brand path resolution
9
+ class Config
10
+ class << self
11
+ # Get the root directory for all video projects
12
+ # @return [String] Absolute path to video-projects directory
13
+ def projects_root
14
+ # Use settings.json configuration
15
+ Appydave::Tools::Configuration::Config.configure
16
+ root = Appydave::Tools::Configuration::Config.settings.video_projects_root
17
+
18
+ return root if root && !root.empty? && Dir.exist?(root)
19
+
20
+ # Fall back to auto-detection if not configured
21
+ detect_projects_root
22
+ end
23
+
24
+ # Get the full path to a brand directory
25
+ # @param brand_key [String] Brand key (e.g., 'appydave', 'voz')
26
+ # @return [String] Absolute path to brand directory
27
+ def brand_path(brand_key)
28
+ Appydave::Tools::Configuration::Config.configure
29
+ brand_info = Appydave::Tools::Configuration::Config.brands.get_brand(brand_key)
30
+
31
+ # If brand has configured video_projects path, use it
32
+ return brand_info.locations.video_projects if brand_info.locations.video_projects && !brand_info.locations.video_projects.empty? && Dir.exist?(brand_info.locations.video_projects)
33
+
34
+ # Fall back to projects_root + expanded brand name
35
+ brand = expand_brand(brand_key)
36
+ path = File.join(projects_root, brand)
37
+
38
+ raise "Brand directory not found: #{path}\nAvailable brands: #{available_brands.join(', ')}" unless Dir.exist?(path)
39
+
40
+ path
41
+ end
42
+
43
+ # Expand brand shortcut to full brand name
44
+ # Reads from brands.json if available, falls back to hardcoded shortcuts
45
+ # @param shortcut [String] Brand shortcut (e.g., 'appydave')
46
+ # @return [String] Full brand name (e.g., 'v-appydave')
47
+ def expand_brand(shortcut)
48
+ return shortcut if shortcut.start_with?('v-')
49
+
50
+ # Try to read from brands.json
51
+ Appydave::Tools::Configuration::Config.configure
52
+ brands_config = Appydave::Tools::Configuration::Config.brands
53
+
54
+ # Check if this shortcut exists in brands.json
55
+ if brands_config.shortcut?(shortcut)
56
+ brand = brands_config.brands.find { |b| b.shortcut == shortcut }
57
+ return "v-#{brand.key}" if brand
58
+ end
59
+
60
+ # Fall back to hardcoded shortcuts for backwards compatibility
61
+ case shortcut
62
+ when 'joy' then 'v-beauty-and-joy'
63
+ when 'ss' then 'v-supportsignal'
64
+ else
65
+ "v-#{shortcut}"
66
+ end
67
+ end
68
+
69
+ # Get list of available brands
70
+ # Reads from brands.json if available, falls back to filesystem scan
71
+ # @return [Array<String>] List of brand shortcuts
72
+ def available_brands
73
+ Appydave::Tools::Configuration::Config.configure
74
+ brands_config = Appydave::Tools::Configuration::Config.brands
75
+
76
+ # If brands are configured in brands.json, use those
77
+ configured_brands = brands_config.brands
78
+ return configured_brands.map(&:shortcut).sort unless configured_brands.empty?
79
+
80
+ # Fall back to filesystem scan
81
+ root = projects_root
82
+ return [] unless Dir.exist?(root)
83
+
84
+ Dir.glob("#{root}/v-*")
85
+ .select { |path| File.directory?(path) }
86
+ .reject { |path| File.basename(path) == 'v-shared' } # Exclude infrastructure
87
+ .select { |path| valid_brand?(path) }
88
+ .map { |path| File.basename(path) }
89
+ .map { |brand| brand.sub(/^v-/, '') }
90
+ .sort
91
+ end
92
+
93
+ # Check if directory is a valid brand
94
+ # @param brand_path [String] Full path to potential brand directory
95
+ # @return [Boolean] true if valid brand
96
+ def valid_brand?(brand_path)
97
+ # A valid brand is a v-* directory that contains project subdirectories
98
+ # (This allows brands in development without .video-tools.env yet)
99
+
100
+ # Must have at least one subdirectory that looks like a project
101
+ Dir.glob("#{brand_path}/*")
102
+ .select { |path| File.directory?(path) }
103
+ .reject { |path| File.basename(path).start_with?('.', '_') }
104
+ .any?
105
+ end
106
+
107
+ # Validate that VIDEO_PROJECTS_ROOT is configured
108
+ # @return [Boolean] true if configured and exists
109
+ def configured?
110
+ Appydave::Tools::Configuration::Config.configure
111
+ root = Appydave::Tools::Configuration::Config.settings.video_projects_root
112
+ !root.nil? && !root.empty? && Dir.exist?(root)
113
+ end
114
+
115
+ private
116
+
117
+ # Auto-detect projects root by finding git repos
118
+ # @return [String] Detected path or raises error
119
+ def detect_projects_root
120
+ # Try to find v-shared in parent directories
121
+ current = Dir.pwd
122
+ 5.times do
123
+ test_path = File.join(current, 'v-shared')
124
+ return File.dirname(test_path) if Dir.exist?(test_path) && Dir.exist?(File.join(test_path, 'video-asset-tools'))
125
+
126
+ parent = File.dirname(current)
127
+ break if parent == current
128
+
129
+ current = parent
130
+ end
131
+
132
+ raise <<~ERROR
133
+ ❌ VIDEO_PROJECTS_ROOT not configured!
134
+
135
+ Configure it using:
136
+ ad_config -e
137
+
138
+ Then add to settings.json:
139
+ "video-projects-root": "/path/to/your/video-projects"
140
+
141
+ Or use ad_config command:
142
+ # (From Ruby console)
143
+ config = Appydave::Tools::Configuration::Config
144
+ config.configure
145
+ config.settings.set('video-projects-root', '/path/to/your/video-projects')
146
+ config.save
147
+ ERROR
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appydave
4
+ module Tools
5
+ module Vat
6
+ # Configuration loader for video asset tools
7
+ # Loads settings from .video-tools.env file in the repository root
8
+ #
9
+ # Usage:
10
+ # config = ConfigLoader.load_from_repo(repo_path)
11
+ # ssd_base = config['SSD_BASE']
12
+ class ConfigLoader
13
+ class ConfigNotFoundError < StandardError; end
14
+ class InvalidConfigError < StandardError; end
15
+
16
+ CONFIG_FILENAME = '.video-tools.env'
17
+ REQUIRED_KEYS = %w[SSD_BASE].freeze
18
+
19
+ class << self
20
+ # Load configuration from a repository path
21
+ # @param repo_path [String] Path to the video project repository
22
+ # @return [Hash] Configuration hash
23
+ def load_from_repo(repo_path)
24
+ config_path = File.join(repo_path, CONFIG_FILENAME)
25
+
26
+ unless File.exist?(config_path)
27
+ raise ConfigNotFoundError, <<~ERROR
28
+ ❌ Configuration file not found: #{config_path}
29
+
30
+ Create a .video-tools.env file in your repository root with:
31
+
32
+ SSD_BASE=/Volumes/T7/youtube-PUBLISHED/appydave
33
+
34
+ See .env.example for a full template.
35
+ ERROR
36
+ end
37
+
38
+ config = parse_env_file(config_path)
39
+ validate_config!(config)
40
+ config
41
+ end
42
+
43
+ private
44
+
45
+ # Parse a .env file into a hash
46
+ # @param file_path [String] Path to .env file
47
+ # @return [Hash] Parsed key-value pairs
48
+ def parse_env_file(file_path)
49
+ config = {}
50
+
51
+ File.readlines(file_path).each do |line|
52
+ line = line.strip
53
+
54
+ # Skip comments and empty lines
55
+ next if line.empty? || line.start_with?('#')
56
+
57
+ # Parse KEY=value
58
+ next unless line =~ /^([A-Z_]+)=(.*)$/
59
+
60
+ key = ::Regexp.last_match(1)
61
+ value = ::Regexp.last_match(2)
62
+
63
+ # Remove quotes if present
64
+ value = value.gsub(/^["']|["']$/, '')
65
+
66
+ config[key] = value
67
+ end
68
+
69
+ config
70
+ end
71
+
72
+ # Validate required configuration keys
73
+ # @param config [Hash] Configuration to validate
74
+ # @raise [InvalidConfigError] if required keys are missing
75
+ def validate_config!(config)
76
+ missing_keys = REQUIRED_KEYS - config.keys
77
+
78
+ return if missing_keys.empty?
79
+
80
+ raise InvalidConfigError, <<~ERROR
81
+ ❌ Missing required configuration keys: #{missing_keys.join(', ')}
82
+
83
+ Your .video-tools.env must include:
84
+ #{REQUIRED_KEYS.map { |k| " #{k}=..." }.join("\n")}
85
+ ERROR
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end