appydave-tools 0.15.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 +19 -0
- data/CLAUDE.md +318 -79
- data/README.md +390 -81
- data/bin/archive_project.rb +249 -0
- data/bin/configuration.rb +21 -1
- data/bin/generate_manifest.rb +357 -0
- data/bin/subtitle_manager.rb +18 -12
- data/bin/subtitle_processor.rb +158 -0
- data/bin/sync_from_ssd.rb +236 -0
- data/bin/vat +623 -0
- data/docs/README.md +169 -0
- data/docs/archive/codebase-audit-2025-01.md +424 -0
- data/docs/archive/documentation-framework-proposal.md +808 -0
- data/docs/archive/purpose-and-philosophy.md +110 -0
- data/docs/archive/test-coverage-quick-wins.md +342 -0
- data/docs/archive/tool-discovery.md +199 -0
- data/docs/archive/tool-documentation-analysis.md +592 -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/tools/bank-reconciliation.md +269 -0
- data/docs/tools/cli-actions.md +444 -0
- data/docs/tools/configuration.md +329 -0
- data/docs/{usage → tools}/gpt-context.md +118 -7
- data/docs/tools/index.md +324 -0
- data/docs/tools/move-images.md +295 -0
- data/docs/tools/name-manager.md +322 -0
- data/docs/tools/prompt-tools.md +209 -0
- data/docs/tools/subtitle-processor.md +242 -0
- data/docs/tools/youtube-automation.md +258 -0
- data/docs/tools/youtube-manager.md +248 -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/{subtitle_manager → subtitle_processor}/clean.rb +1 -1
- data/lib/appydave/tools/{subtitle_manager → subtitle_processor}/join.rb +5 -2
- 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 +11 -5
- data/package.json +1 -1
- metadata +85 -14
- data/docs/dam/overview.md +0 -28
- data/lib/mj-paste-test/main.rb +0 -35
- data/lib/mj-paste-test/prompts.txt +0 -18
- data/lib/mj-paste-test/readme-leonardo.md +0 -0
- /data/lib/appydave/tools/{subtitle_manager → subtitle_processor}/_doc-clean.md +0 -0
- /data/lib/appydave/tools/{subtitle_manager → subtitle_processor}/_doc-join.md +0 -0
- /data/lib/appydave/tools/{subtitle_manager → subtitle_processor}/_doc-todo.md +0 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'optparse'
|
|
5
|
+
|
|
6
|
+
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
|
|
7
|
+
|
|
8
|
+
require 'appydave/tools'
|
|
9
|
+
|
|
10
|
+
# Process command line arguments for SubtitleProcessor operations
|
|
11
|
+
class SubtitleProcessorCLI
|
|
12
|
+
def initialize
|
|
13
|
+
@commands = {
|
|
14
|
+
'clean' => method(:clean_subtitles),
|
|
15
|
+
'join' => method(:join_subtitles)
|
|
16
|
+
}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def run
|
|
20
|
+
command, *args = ARGV
|
|
21
|
+
if command.nil?
|
|
22
|
+
puts 'No command provided. Use -h for help.'
|
|
23
|
+
print_help
|
|
24
|
+
exit
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
if @commands.key?(command)
|
|
28
|
+
@commands[command].call(args)
|
|
29
|
+
else
|
|
30
|
+
puts "Unknown command: #{command}"
|
|
31
|
+
print_help
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def clean_subtitles(args)
|
|
38
|
+
options = { file: nil, output: nil }
|
|
39
|
+
|
|
40
|
+
# Command-specific option parser
|
|
41
|
+
clean_parser = OptionParser.new do |opts|
|
|
42
|
+
opts.banner = 'Usage: subtitle_processor.rb clean [options]'
|
|
43
|
+
|
|
44
|
+
opts.on('-f', '--file FILE', 'SRT file to process') do |v|
|
|
45
|
+
options[:file] = v
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
opts.on('-o', '--output FILE', 'Output file') do |v|
|
|
49
|
+
options[:output] = v
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
opts.on('-h', '--help', 'Show this message') do
|
|
53
|
+
puts opts
|
|
54
|
+
exit
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
begin
|
|
59
|
+
clean_parser.parse!(args)
|
|
60
|
+
rescue OptionParser::InvalidOption => e
|
|
61
|
+
puts "Error: #{e.message}"
|
|
62
|
+
puts clean_parser
|
|
63
|
+
exit
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Validate required options
|
|
67
|
+
if options[:file].nil? || options[:output].nil?
|
|
68
|
+
puts 'Error: Missing required options.'
|
|
69
|
+
puts clean_parser
|
|
70
|
+
exit
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Assuming `Appydave::Tools::SubtitleProcessor::Clean` exists
|
|
74
|
+
cleaner = Appydave::Tools::SubtitleProcessor::Clean.new(file_path: options[:file])
|
|
75
|
+
cleaner.clean
|
|
76
|
+
cleaner.write(options[:output])
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def join_subtitles(args)
|
|
80
|
+
options = {
|
|
81
|
+
folder: './',
|
|
82
|
+
files: '*.srt',
|
|
83
|
+
sort: 'inferred',
|
|
84
|
+
buffer: 100,
|
|
85
|
+
output: 'merged.srt',
|
|
86
|
+
verbose: false
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
join_parser = OptionParser.new do |opts|
|
|
90
|
+
opts.banner = 'Usage: subtitle_processor.rb join [options]'
|
|
91
|
+
|
|
92
|
+
opts.on('-d', '--directory DIR', 'Directory containing SRT files (default: current directory)') do |v|
|
|
93
|
+
options[:folder] = v
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
opts.on('-f', '--files PATTERN', 'File pattern (e.g., "*.srt" or "part1.srt,part2.srt")') do |v|
|
|
97
|
+
options[:files] = v
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
opts.on('-s', '--sort ORDER', %w[asc desc inferred], 'Sort order (asc/desc/inferred)') do |v|
|
|
101
|
+
options[:sort] = v
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
opts.on('-b', '--buffer MS', Integer, 'Buffer between merged files in milliseconds') do |v|
|
|
105
|
+
options[:buffer] = v
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
opts.on('-o', '--output FILE', 'Output file') do |v|
|
|
109
|
+
options[:output] = v
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
opts.on('-L', '--log-level LEVEL', %w[none info detail], 'Log level (default: info)') do |v|
|
|
113
|
+
options[:log_level] = v.to_sym
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
opts.on('-h', '--help', 'Show this message') do
|
|
117
|
+
puts opts
|
|
118
|
+
exit
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
begin
|
|
123
|
+
join_parser.parse!(args)
|
|
124
|
+
rescue OptionParser::InvalidOption => e
|
|
125
|
+
puts "Error: #{e.message}"
|
|
126
|
+
puts join_parser
|
|
127
|
+
exit
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Validate required options
|
|
131
|
+
if options[:folder].nil? || options[:files].nil? || options[:output].nil?
|
|
132
|
+
puts 'Error: Missing required options.'
|
|
133
|
+
puts join_parser
|
|
134
|
+
exit
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Assuming `Appydave::Tools::SubtitleProcessor::Join` exists
|
|
138
|
+
joiner = Appydave::Tools::SubtitleProcessor::Join.new(
|
|
139
|
+
folder: options[:folder],
|
|
140
|
+
files: options[:files],
|
|
141
|
+
sort: options[:sort],
|
|
142
|
+
buffer: options[:buffer],
|
|
143
|
+
output: options[:output],
|
|
144
|
+
log_level: options[:log_level]
|
|
145
|
+
)
|
|
146
|
+
joiner.join
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def print_help
|
|
150
|
+
puts 'Usage: subtitle_processor.rb [command] [options]'
|
|
151
|
+
puts 'Commands:'
|
|
152
|
+
puts ' clean Clean and normalize SRT files'
|
|
153
|
+
puts ' join Join multiple SRT files'
|
|
154
|
+
puts "Run 'subtitle_processor.rb [command] --help' for more information on a command."
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
SubtitleProcessorCLI.new.run
|
|
@@ -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?
|