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,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
|
|
|
@@ -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
|