appydave-tools 0.76.5 → 0.76.7

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0531bc72c6c4aeac9830f72f552a4d683775d7efc61ee483069d882cdbbbf5af
4
- data.tar.gz: 1c8e032ddc4e8dfc4c196452465abea40958107d2d68c5e4063c1ec2c36a3a9d
3
+ metadata.gz: 51ac1542f215f90d518a24c96b5185cf954b9db2beab4bbb93c8d43f3b106fb7
4
+ data.tar.gz: 11acb6d8817125846945e4f7ba3bfb77b305155093f507f346a21477c4102cdb
5
5
  SHA512:
6
- metadata.gz: 1787f781bdf758fae98c3388cce0437aca330a3161937f5c214651885c80e05598682e64afc3adea1e2ffd3b2f70d09287a9acca12c8fada89077b7d48791a39
7
- data.tar.gz: 6cadabed0aa0f0be9d993b4dbda2c304c9a34f717fcc7b86b8cc059777e7cb0a57a494ddc2c3e5432fd4d4d85990330a6d401aa75c89bb7b0be5f5207c68d12b
6
+ metadata.gz: 02a5b3623de62bbb09d343101c1edc48b711fc8682d9396689043afcdd134276c887de57d802e8a717db9adaeb34b48eb14dd6a199e085d64430a0525300cf1e
7
+ data.tar.gz: ce60ac62a7753a206768335c7cc51c8d4765223efd9dd34d4a490fc250f8af63aca11fd532f9918f15ac075be8e7698bd7613704040a16f66777a0455ca54c25
data/CHANGELOG.md CHANGED
@@ -1,3 +1,21 @@
1
+ ## [0.76.6](https://github.com/appydave/appydave-tools/compare/v0.76.5...v0.76.6) (2026-03-19)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * extract S3ScanCommand class from VatCLI; add smoke-test spec ([cd03606](https://github.com/appydave/appydave-tools/commit/cd03606e3307665c7f69391d6c160f50df1773ba))
7
+ * remove redundant rubocop disable directives from S3ScanCommand (CI rubocop 1.85.1) ([4c9deb4](https://github.com/appydave/appydave-tools/commit/4c9deb4298d4577a99d0e82e7b72918a26275d40))
8
+
9
+ ## [0.76.5](https://github.com/appydave/appydave-tools/compare/v0.76.4...v0.76.5) (2026-03-19)
10
+
11
+
12
+ ### Bug Fixes
13
+
14
+ * extract LocalSyncStatus module from VatCLI; add spec ([0672855](https://github.com/appydave/appydave-tools/commit/0672855a25ebcbb2df3f5eea6208f910aed74124))
15
+ * remove redundant rubocop disable directives in LocalSyncStatus ([9050795](https://github.com/appydave/appydave-tools/commit/905079573caf7555771da02b2d9c6ebfc34d1c95))
16
+ * replace format_bytes with FileHelper.format_size; remove duplicate method from VatCLI ([3cd362f](https://github.com/appydave/appydave-tools/commit/3cd362f79342578ddb7a750b9b404851cf4d1eb4))
17
+ * restore youtube_automation_config require removed by micro-cleanup; fix spec brand dir setup ([a5779a7](https://github.com/appydave/appydave-tools/commit/a5779a7b5dab1697b2a21ca3bbece9c0bff340c2))
18
+
1
19
  ## [0.76.4](https://github.com/appydave/appydave-tools/compare/v0.76.3...v0.76.4) (2026-03-19)
2
20
 
3
21
 
data/bin/dam CHANGED
@@ -154,7 +154,7 @@ class VatCLI
154
154
 
155
155
  # S3 Upload
156
156
  def s3_up_command(args)
157
- options = parse_s3_args(args, 's3-up')
157
+ options = Appydave::Tools::Dam::S3ArgParser.parse_s3(args, 's3-up')
158
158
  s3_ops = Appydave::Tools::Dam::S3Operations.new(options[:brand], options[:project])
159
159
  s3_ops.upload(dry_run: options[:dry_run])
160
160
  rescue StandardError => e
@@ -164,7 +164,7 @@ class VatCLI
164
164
 
165
165
  # S3 Download
166
166
  def s3_down_command(args)
167
- options = parse_s3_args(args, 's3-down')
167
+ options = Appydave::Tools::Dam::S3ArgParser.parse_s3(args, 's3-down')
168
168
  s3_ops = Appydave::Tools::Dam::S3Operations.new(options[:brand], options[:project])
169
169
  s3_ops.download(dry_run: options[:dry_run])
170
170
  rescue StandardError => e
@@ -174,7 +174,7 @@ class VatCLI
174
174
 
175
175
  # S3 Status
176
176
  def s3_status_command(args)
177
- options = parse_s3_args(args, 's3-status')
177
+ options = Appydave::Tools::Dam::S3ArgParser.parse_s3(args, 's3-status')
178
178
  s3_ops = Appydave::Tools::Dam::S3Operations.new(options[:brand], options[:project])
179
179
  s3_ops.status
180
180
  rescue StandardError => e
@@ -184,7 +184,7 @@ class VatCLI
184
184
 
185
185
  # S3 Cleanup Remote
186
186
  def s3_cleanup_remote_command(args)
187
- options = parse_s3_args(args, 's3-cleanup-remote')
187
+ options = Appydave::Tools::Dam::S3ArgParser.parse_s3(args, 's3-cleanup-remote')
188
188
  s3_ops = Appydave::Tools::Dam::S3Operations.new(options[:brand], options[:project])
189
189
  s3_ops.cleanup(force: options[:force], dry_run: options[:dry_run])
190
190
  rescue StandardError => e
@@ -194,7 +194,7 @@ class VatCLI
194
194
 
195
195
  # S3 Cleanup Local
196
196
  def s3_cleanup_local_command(args)
197
- options = parse_s3_args(args, 's3-cleanup-local')
197
+ options = Appydave::Tools::Dam::S3ArgParser.parse_s3(args, 's3-cleanup-local')
198
198
  s3_ops = Appydave::Tools::Dam::S3Operations.new(options[:brand], options[:project])
199
199
  s3_ops.cleanup_local(force: options[:force], dry_run: options[:dry_run])
200
200
  rescue StandardError => e
@@ -204,7 +204,7 @@ class VatCLI
204
204
 
205
205
  # Share file via pre-signed URL
206
206
  def s3_share_command(args)
207
- options = parse_share_args(args)
207
+ options = Appydave::Tools::Dam::S3ArgParser.parse_share(args)
208
208
 
209
209
  share_ops = Appydave::Tools::Dam::ShareOperations.new(options[:brand], options[:project])
210
210
  share_ops.generate_links(files: options[:file], expires: options[:expires], download: options[:download])
@@ -215,7 +215,7 @@ class VatCLI
215
215
 
216
216
  # Discover files in S3 for a project
217
217
  def s3_discover_command(args)
218
- options = parse_discover_args(args)
218
+ options = Appydave::Tools::Dam::S3ArgParser.parse_discover(args)
219
219
  files = fetch_s3_files(options[:brand_key], options[:project_id])
220
220
 
221
221
  return if handle_empty_files?(files, options[:brand_key], options[:project_id])
@@ -228,7 +228,7 @@ class VatCLI
228
228
 
229
229
  # Archive project to SSD
230
230
  def archive_command(args)
231
- options = parse_s3_args(args, 'archive')
231
+ options = Appydave::Tools::Dam::S3ArgParser.parse_s3(args, 'archive')
232
232
  s3_ops = Appydave::Tools::Dam::S3Operations.new(options[:brand], options[:project])
233
233
  s3_ops.archive(force: options[:force], dry_run: options[:dry_run])
234
234
  rescue StandardError => e
@@ -461,132 +461,6 @@ class VatCLI
461
461
  exit 1
462
462
  end
463
463
 
464
- # Parse S3 command arguments
465
- # rubocop:disable Metrics/MethodLength
466
- def parse_s3_args(args, command)
467
- dry_run = args.include?('--dry-run')
468
- force = args.include?('--force')
469
- args = args.reject { |arg| arg.start_with?('--') }
470
-
471
- brand_arg = args[0]
472
- project_arg = args[1]
473
-
474
- if brand_arg.nil?
475
- # Auto-detect from PWD
476
- brand, project_id = Appydave::Tools::Dam::ProjectResolver.detect_from_pwd
477
- if brand.nil? || project_id.nil?
478
- puts '❌ Could not auto-detect brand/project from current directory'
479
- puts "Usage: dam #{command} <brand> <project> [--dry-run]"
480
- exit 1
481
- end
482
- brand_key = brand # Already detected, use as-is
483
- else
484
- # Validate brand exists before trying to resolve project
485
- unless valid_brand?(brand_arg)
486
- puts "❌ Invalid brand: '#{brand_arg}'"
487
- puts ''
488
- puts 'Valid brands:'
489
- puts ' appydave → v-appydave (AppyDave brand)'
490
- puts ' voz → v-voz (VOZ client)'
491
- puts ' aitldr → v-aitldr (AITLDR brand)'
492
- puts ' kiros → v-kiros (Kiros client)'
493
- puts ' joy → v-beauty-and-joy (Beauty & Joy)'
494
- puts ' ss → v-supportsignal (SupportSignal)'
495
- puts ''
496
- puts "Usage: dam #{command} <brand> <project> [--dry-run]"
497
- exit 1
498
- end
499
-
500
- brand_key = brand_arg # Use the shortcut/key (e.g., 'appydave')
501
- brand = Appydave::Tools::Dam::Config.expand_brand(brand_arg) # Expand for path resolution
502
- project_id = Appydave::Tools::Dam::ProjectResolver.resolve(brand_arg, project_arg)
503
- end
504
-
505
- # Set ENV for compatibility with ConfigLoader
506
- ENV['BRAND_PATH'] = Appydave::Tools::Dam::Config.brand_path(brand)
507
-
508
- { brand: brand_key, project: project_id, dry_run: dry_run, force: force }
509
- end
510
- # rubocop:enable Metrics/MethodLength
511
-
512
- def valid_brand?(brand_key)
513
- Appydave::Tools::Configuration::Config.configure
514
- brands = Appydave::Tools::Configuration::Config.brands
515
- brands.key?(brand_key) || brands.shortcut?(brand_key)
516
- end
517
-
518
- def parse_share_args(args)
519
- # Extract --expires flag
520
- expires = '7d' # default
521
- if (expires_index = args.index('--expires'))
522
- expires = args[expires_index + 1]
523
- args.delete_at(expires_index + 1)
524
- args.delete_at(expires_index)
525
- end
526
-
527
- # Extract --download flag
528
- download = args.include?('--download')
529
-
530
- # Remove other flags
531
- args = args.reject { |arg| arg.start_with?('--') }
532
-
533
- brand_arg = args[0]
534
- project_arg = args[1]
535
- file_arg = args[2]
536
-
537
- show_share_usage_and_exit if brand_arg.nil? || project_arg.nil? || file_arg.nil?
538
-
539
- brand_key = brand_arg
540
- brand = Appydave::Tools::Dam::Config.expand_brand(brand_arg)
541
- project_id = Appydave::Tools::Dam::ProjectResolver.resolve(brand_arg, project_arg)
542
-
543
- # Set ENV for compatibility with ConfigLoader
544
- ENV['BRAND_PATH'] = Appydave::Tools::Dam::Config.brand_path(brand)
545
-
546
- { brand: brand_key, project: project_id, file: file_arg, expires: expires, download: download }
547
- end
548
-
549
- def show_share_usage_and_exit
550
- puts 'Usage: dam s3-share <brand> <project> <file> [--expires 7d] [--download]'
551
- puts ''
552
- puts 'Options:'
553
- puts ' --expires TIME Expiry time (default: 7d)'
554
- puts ' --download Force download instead of viewing in browser'
555
- puts ''
556
- puts 'Examples:'
557
- puts ' dam s3-share appydave b70 video.mp4'
558
- puts ' dam s3-share appydave b70 video.mp4 --expires 24h'
559
- puts ' dam s3-share appydave b70 video.mp4 --download'
560
- puts ' dam s3-share voz boy-baker final-edit.mov --expires 3d --download'
561
- exit 1
562
- end
563
-
564
- def parse_discover_args(args)
565
- shareable = args.include?('--shareable')
566
- args = args.reject { |arg| arg.start_with?('--') }
567
-
568
- brand_arg = args[0]
569
- project_arg = args[1]
570
-
571
- if brand_arg.nil? || project_arg.nil?
572
- puts 'Usage: dam s3-discover <brand> <project> [--shareable]'
573
- puts ''
574
- puts 'Examples:'
575
- puts ' dam s3-discover appydave b70 # List files'
576
- puts ' dam s3-discover appydave b70 --shareable # Generate share commands'
577
- exit 1
578
- end
579
-
580
- brand_key = brand_arg
581
- brand = Appydave::Tools::Dam::Config.expand_brand(brand_arg)
582
- project_id = Appydave::Tools::Dam::ProjectResolver.resolve(brand_arg, project_arg)
583
-
584
- # Set ENV for compatibility with ConfigLoader
585
- ENV['BRAND_PATH'] = Appydave::Tools::Dam::Config.brand_path(brand)
586
-
587
- { brand_key: brand_key, project_id: project_id, shareable: shareable }
588
- end
589
-
590
464
  def fetch_s3_files(brand_key, project_id)
591
465
  s3_ops = Appydave::Tools::Dam::S3Operations.new(brand_key, project_id)
592
466
  s3_ops.list_s3_files
@@ -1325,9 +1199,9 @@ class VatCLI
1325
1199
  brand_arg = args[0]
1326
1200
 
1327
1201
  if all_brands
1328
- scan_all_brands_s3
1202
+ Appydave::Tools::Dam::S3ScanCommand.new.scan_all
1329
1203
  elsif brand_arg
1330
- scan_single_brand_s3(brand_arg)
1204
+ Appydave::Tools::Dam::S3ScanCommand.new.scan_single(brand_arg)
1331
1205
  else
1332
1206
  puts 'Usage: dam s3-scan <brand> [--all]'
1333
1207
  puts ''
@@ -1343,188 +1217,6 @@ class VatCLI
1343
1217
  puts e.backtrace.first(5).join("\n") if ENV['DEBUG']
1344
1218
  exit 1
1345
1219
  end
1346
-
1347
- # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity
1348
- def scan_single_brand_s3(brand_arg)
1349
- puts "🔄 Scanning S3 for #{brand_arg}..."
1350
- puts ''
1351
-
1352
- brand_key = brand_arg
1353
- scanner = Appydave::Tools::Dam::S3Scanner.new(brand_key)
1354
-
1355
- # Get brand info for S3 path
1356
- Appydave::Tools::Configuration::Config.configure
1357
- brand_info = Appydave::Tools::Configuration::Config.brands.get_brand(brand_key)
1358
- bucket = brand_info.aws.s3_bucket
1359
- prefix = brand_info.aws.s3_prefix
1360
- region = brand_info.aws.region
1361
-
1362
- # Spinner characters for progress
1363
- spinner_chars = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
1364
- spinner_index = 0
1365
-
1366
- print "🔍 Scanning s3://#{bucket}/#{prefix}\n"
1367
- print ' Scanning projects... '
1368
-
1369
- # Scan all projects with progress callback
1370
- results = scanner.scan_all_projects(show_progress: false) do |current, total|
1371
- print "\r Scanning projects... #{spinner_chars[spinner_index]} (#{current}/#{total})"
1372
- spinner_index = (spinner_index + 1) % spinner_chars.length
1373
- end
1374
-
1375
- print "\r Scanning projects... ✓ (#{results.size} found)\n"
1376
- puts ''
1377
-
1378
- if results.empty?
1379
- puts "⚠️ No projects found in S3 for #{brand_key}"
1380
- puts ' This may indicate:'
1381
- puts ' - No files uploaded to S3 yet'
1382
- puts ' - S3 bucket or prefix misconfigured'
1383
- puts ' - AWS credentials issue'
1384
- return
1385
- end
1386
-
1387
- # Load existing manifest
1388
- brand_path = Appydave::Tools::Dam::Config.brand_path(brand_key)
1389
- manifest_path = File.join(brand_path, 'projects.json')
1390
-
1391
- unless File.exist?(manifest_path)
1392
- puts "❌ Manifest not found: #{manifest_path}"
1393
- puts " Run: dam manifest #{brand_key}"
1394
- puts " Then retry: dam s3-scan #{brand_key}"
1395
- exit 1
1396
- end
1397
-
1398
- manifest = JSON.parse(File.read(manifest_path), symbolize_names: true)
1399
-
1400
- # Identify matched and orphaned S3 projects
1401
- local_project_ids = manifest[:projects].map { |p| p[:id] }
1402
- matched_projects = results.slice(*local_project_ids)
1403
- orphaned_projects = results.reject { |project_id, _| local_project_ids.include?(project_id) }
1404
-
1405
- # Merge S3 scan data into manifest for matched projects
1406
- updated_count = 0
1407
- manifest[:projects].each do |project|
1408
- project_id = project[:id]
1409
- s3_data = results[project_id]
1410
- next unless s3_data
1411
-
1412
- project[:storage][:s3] = s3_data
1413
- updated_count += 1
1414
- end
1415
-
1416
- # Update timestamp and note
1417
- manifest[:config][:last_updated] = Time.now.utc.iso8601
1418
- manifest[:config][:note] = 'Auto-generated manifest with S3 scan data. Regenerate with: dam s3-scan'
1419
-
1420
- # Write updated manifest
1421
- File.write(manifest_path, JSON.pretty_generate(manifest))
1422
-
1423
- # Add local sync status to matched projects
1424
- Appydave::Tools::Dam::LocalSyncStatus.enrich!(matched_projects, brand_key)
1425
-
1426
- # Display table
1427
- display_s3_scan_table(matched_projects, orphaned_projects, bucket, prefix, region)
1428
-
1429
- # Summary
1430
- total_manifest_projects = manifest[:projects].size
1431
- missing_count = total_manifest_projects - matched_projects.size
1432
-
1433
- puts ''
1434
- puts 'ℹ️ Summary:'
1435
- puts " • Updated #{updated_count} projects in manifest"
1436
- puts " • #{missing_count} local project(s) not yet uploaded to S3" if missing_count.positive?
1437
- puts " • Manifest: #{manifest_path}"
1438
- puts ''
1439
- end
1440
- # rubocop:enable Metrics/MethodLength, Metrics/CyclomaticComplexity
1441
-
1442
- # Display S3 scan results in table format
1443
- # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity, Style/FormatStringToken
1444
- def display_s3_scan_table(matched_projects, orphaned_projects, bucket, prefix, region)
1445
- puts '✅ S3 Projects Report'
1446
- puts ''
1447
- puts 'PROJECT FILES SIZE LOCAL S3 MODIFIED'
1448
- puts '-' * 92
1449
-
1450
- # Display matched projects first (sorted alphabetically)
1451
- matched_projects.sort.each do |project_id, data|
1452
- files = data[:file_count].to_s.rjust(5)
1453
- size = Appydave::Tools::Dam::FileHelper.format_size(data[:total_bytes]).rjust(10)
1454
- local_status = Appydave::Tools::Dam::LocalSyncStatus.format(data[:local_status], data[:local_file_count], data[:file_count])
1455
- modified = data[:last_modified] ? Time.parse(data[:last_modified]).strftime('%Y-%m-%d %H:%M') : 'N/A'
1456
-
1457
- puts format('%-36s %5s %10s %-9s %s', project_id, files, size, local_status, modified)
1458
- end
1459
-
1460
- # Display orphaned projects (sorted alphabetically)
1461
- return if orphaned_projects.empty?
1462
-
1463
- puts '-' * 92
1464
- orphaned_projects.sort.each do |project_id, data|
1465
- files = data[:file_count].to_s.rjust(5)
1466
- size = Appydave::Tools::Dam::FileHelper.format_size(data[:total_bytes]).rjust(10)
1467
- local_status = 'N/A'
1468
- modified = data[:last_modified] ? Time.parse(data[:last_modified]).strftime('%Y-%m-%d %H:%M') : 'N/A'
1469
-
1470
- puts format('%-36s %5s %10s %-9s %s', project_id, files, size, local_status, modified)
1471
- end
1472
-
1473
- puts ''
1474
- folder_word = orphaned_projects.size > 1 ? 'folders' : 'folder'
1475
- puts "⚠️ #{orphaned_projects.size} orphaned #{folder_word} found (no local project)"
1476
-
1477
- orphaned_projects.sort.each do |project_id, _data|
1478
- # Build AWS Console URL
1479
- project_prefix = "#{prefix}#{project_id}/"
1480
- console_url = "https://#{region}.console.aws.amazon.com/s3/buckets/#{bucket}?prefix=#{project_prefix}&region=#{region}"
1481
- puts " → #{project_id}"
1482
- puts " #{console_url}"
1483
- end
1484
- end
1485
- # rubocop:enable Metrics/MethodLength, Metrics/CyclomaticComplexity, Style/FormatStringToken
1486
-
1487
- # rubocop:disable Metrics/MethodLength
1488
- def scan_all_brands_s3
1489
- Appydave::Tools::Configuration::Config.configure
1490
- brands_config = Appydave::Tools::Configuration::Config.brands
1491
-
1492
- results = []
1493
- brands_config.brands.each do |brand_info|
1494
- brand_key = brand_info.key
1495
- puts ''
1496
- puts '=' * 60
1497
-
1498
- begin
1499
- scan_single_brand_s3(brand_key)
1500
- results << { brand: brand_key, success: true }
1501
- rescue StandardError => e
1502
- puts "❌ Failed to scan #{brand_key}: #{e.message}"
1503
- results << { brand: brand_key, success: false, error: e.message }
1504
- end
1505
- end
1506
-
1507
- puts ''
1508
- puts '=' * 60
1509
- puts '📋 Summary - S3 Scans:'
1510
- puts ''
1511
-
1512
- successful, failed = results.partition { |r| r[:success] }
1513
-
1514
- successful.each do |result|
1515
- brand_display = result[:brand].ljust(15)
1516
- puts "✅ #{brand_display} Scanned successfully"
1517
- end
1518
-
1519
- failed.each do |result|
1520
- brand_display = result[:brand].ljust(15)
1521
- puts "❌ #{brand_display} #{result[:error]}"
1522
- end
1523
-
1524
- puts ''
1525
- puts "Total brands scanned: #{successful.size}/#{results.size}"
1526
- end
1527
- # rubocop:enable Metrics/MethodLength
1528
1220
  end
1529
1221
 
1530
1222
  # Run CLI
@@ -9,9 +9,9 @@
9
9
 
10
10
  ## Pending
11
11
  - [x] extract-format-bytes — Replaced 4 callers (plan said 3; orphaned-projects loop in display_s3_scan_table was a 4th). format_bytes deleted. rubocop 0 offenses. Commit: 3cd362f.
12
- - [~] extract-local-sync-status — Extract `add_local_sync_status!` + `format_local_status` new `LocalSyncStatus` module; add spec; update callers in bin/dam
13
- - [ ] extract-s3-scan-command — Extract `scan_single_brand_s3` + `scan_all_brands_s3` + `display_s3_scan_table` new `S3ScanCommand` class; add spec; **depends on extract-local-sync-status completing first**
14
- - [ ] extract-s3-arg-parser — Extract `parse_s3_args` + `valid_brand?` + `parse_share_args` + `show_share_usage_and_exit` + `parse_discover_args` → new `S3ArgParser` class; add spec
12
+ - [x] extract-local-sync-status — LocalSyncStatus module created, 7 specs added (838 total), both methods gone from VatCLI. Side-fix: restored youtube_automation_config require incorrectly removed in prior commit. v0.76.5.
13
+ - [x] extract-s3-scan-command — S3ScanCommand created, 2 smoke tests added (840 total), 3 methods gone from VatCLI. Note: rubocop-disable directives became redundant once methods left God class — needed 2nd kfix to remove them. v0.76.6.
14
+ - [~] extract-s3-arg-parser — Extract `parse_s3_args` + `valid_brand?` + `parse_share_args` + `show_share_usage_and_exit` + `parse_discover_args` → new `S3ArgParser` class; add spec
15
15
 
16
16
  ## In Progress
17
17
 
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appydave
4
+ module Tools
5
+ module Dam
6
+ # Parses and validates CLI arguments for S3-related dam commands
7
+ # Handles brand resolution, project lookup, flag extraction, and ENV setup
8
+ module S3ArgParser
9
+ module_function
10
+
11
+ def parse_s3(args, command)
12
+ dry_run = args.include?('--dry-run')
13
+ force = args.include?('--force')
14
+ args = args.reject { |arg| arg.start_with?('--') }
15
+
16
+ brand_arg = args[0]
17
+ project_arg = args[1]
18
+
19
+ if brand_arg.nil?
20
+ # Auto-detect from PWD
21
+ brand, project_id = Appydave::Tools::Dam::ProjectResolver.detect_from_pwd
22
+ if brand.nil? || project_id.nil?
23
+ puts '❌ Could not auto-detect brand/project from current directory'
24
+ puts "Usage: dam #{command} <brand> <project> [--dry-run]"
25
+ exit 1
26
+ end
27
+ brand_key = brand # Already detected, use as-is
28
+ else
29
+ # Validate brand exists before trying to resolve project
30
+ unless valid_brand?(brand_arg)
31
+ puts "❌ Invalid brand: '#{brand_arg}'"
32
+ puts ''
33
+ puts 'Valid brands:'
34
+ puts ' appydave → v-appydave (AppyDave brand)'
35
+ puts ' voz → v-voz (VOZ client)'
36
+ puts ' aitldr → v-aitldr (AITLDR brand)'
37
+ puts ' kiros → v-kiros (Kiros client)'
38
+ puts ' joy → v-beauty-and-joy (Beauty & Joy)'
39
+ puts ' ss → v-supportsignal (SupportSignal)'
40
+ puts ''
41
+ puts "Usage: dam #{command} <brand> <project> [--dry-run]"
42
+ exit 1
43
+ end
44
+
45
+ brand_key = brand_arg # Use the shortcut/key (e.g., 'appydave')
46
+ brand = Appydave::Tools::Dam::Config.expand_brand(brand_arg) # Expand for path resolution
47
+ project_id = Appydave::Tools::Dam::ProjectResolver.resolve(brand_arg, project_arg)
48
+ end
49
+
50
+ # Set ENV for compatibility with ConfigLoader
51
+ ENV['BRAND_PATH'] = Appydave::Tools::Dam::Config.brand_path(brand)
52
+
53
+ { brand: brand_key, project: project_id, dry_run: dry_run, force: force }
54
+ end
55
+
56
+ def parse_share(args)
57
+ # Extract --expires flag
58
+ expires = '7d' # default
59
+ if (expires_index = args.index('--expires'))
60
+ expires = args[expires_index + 1]
61
+ args.delete_at(expires_index + 1)
62
+ args.delete_at(expires_index)
63
+ end
64
+
65
+ # Extract --download flag
66
+ download = args.include?('--download')
67
+
68
+ # Remove other flags
69
+ args = args.reject { |arg| arg.start_with?('--') }
70
+
71
+ brand_arg = args[0]
72
+ project_arg = args[1]
73
+ file_arg = args[2]
74
+
75
+ show_share_usage_and_exit if brand_arg.nil? || project_arg.nil? || file_arg.nil?
76
+
77
+ brand_key = brand_arg
78
+ brand = Appydave::Tools::Dam::Config.expand_brand(brand_arg)
79
+ project_id = Appydave::Tools::Dam::ProjectResolver.resolve(brand_arg, project_arg)
80
+
81
+ # Set ENV for compatibility with ConfigLoader
82
+ ENV['BRAND_PATH'] = Appydave::Tools::Dam::Config.brand_path(brand)
83
+
84
+ { brand: brand_key, project: project_id, file: file_arg, expires: expires, download: download }
85
+ end
86
+
87
+ def parse_discover(args)
88
+ shareable = args.include?('--shareable')
89
+ args = args.reject { |arg| arg.start_with?('--') }
90
+
91
+ brand_arg = args[0]
92
+ project_arg = args[1]
93
+
94
+ if brand_arg.nil? || project_arg.nil?
95
+ puts 'Usage: dam s3-discover <brand> <project> [--shareable]'
96
+ puts ''
97
+ puts 'Examples:'
98
+ puts ' dam s3-discover appydave b70 # List files'
99
+ puts ' dam s3-discover appydave b70 --shareable # Generate share commands'
100
+ exit 1
101
+ end
102
+
103
+ brand_key = brand_arg
104
+ brand = Appydave::Tools::Dam::Config.expand_brand(brand_arg)
105
+ project_id = Appydave::Tools::Dam::ProjectResolver.resolve(brand_arg, project_arg)
106
+
107
+ # Set ENV for compatibility with ConfigLoader
108
+ ENV['BRAND_PATH'] = Appydave::Tools::Dam::Config.brand_path(brand)
109
+
110
+ { brand_key: brand_key, project_id: project_id, shareable: shareable }
111
+ end
112
+
113
+ def valid_brand?(brand_key)
114
+ Appydave::Tools::Configuration::Config.configure
115
+ brands = Appydave::Tools::Configuration::Config.brands
116
+ brands.key?(brand_key) || brands.shortcut?(brand_key)
117
+ end
118
+
119
+ def show_share_usage_and_exit
120
+ puts 'Usage: dam s3-share <brand> <project> <file> [--expires 7d] [--download]'
121
+ puts ''
122
+ puts 'Options:'
123
+ puts ' --expires TIME Expiry time (default: 7d)'
124
+ puts ' --download Force download instead of viewing in browser'
125
+ puts ''
126
+ puts 'Examples:'
127
+ puts ' dam s3-share appydave b70 video.mp4'
128
+ puts ' dam s3-share appydave b70 video.mp4 --expires 24h'
129
+ puts ' dam s3-share appydave b70 video.mp4 --download'
130
+ puts ' dam s3-share voz boy-baker final-edit.mov --expires 3d --download'
131
+ exit 1
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,191 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appydave
4
+ module Tools
5
+ module Dam
6
+ # Encapsulates S3 scan logic: single-brand and all-brands scanning
7
+ class S3ScanCommand
8
+ # Scan a single brand's S3 bucket and update its manifest
9
+ def scan_single(brand_key)
10
+ puts "🔄 Scanning S3 for #{brand_key}..."
11
+ puts ''
12
+
13
+ scanner = Appydave::Tools::Dam::S3Scanner.new(brand_key)
14
+
15
+ # Get brand info for S3 path
16
+ Appydave::Tools::Configuration::Config.configure
17
+ brand_info = Appydave::Tools::Configuration::Config.brands.get_brand(brand_key)
18
+ bucket = brand_info.aws.s3_bucket
19
+ prefix = brand_info.aws.s3_prefix
20
+ region = brand_info.aws.region
21
+
22
+ # Spinner characters for progress
23
+ spinner_chars = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
24
+ spinner_index = 0
25
+
26
+ print "🔍 Scanning s3://#{bucket}/#{prefix}\n"
27
+ print ' Scanning projects... '
28
+
29
+ # Scan all projects with progress callback
30
+ results = scanner.scan_all_projects(show_progress: false) do |current, total|
31
+ print "\r Scanning projects... #{spinner_chars[spinner_index]} (#{current}/#{total})"
32
+ spinner_index = (spinner_index + 1) % spinner_chars.length
33
+ end
34
+
35
+ print "\r Scanning projects... ✓ (#{results.size} found)\n"
36
+ puts ''
37
+
38
+ if results.empty?
39
+ puts "⚠️ No projects found in S3 for #{brand_key}"
40
+ puts ' This may indicate:'
41
+ puts ' - No files uploaded to S3 yet'
42
+ puts ' - S3 bucket or prefix misconfigured'
43
+ puts ' - AWS credentials issue'
44
+ return
45
+ end
46
+
47
+ # Load existing manifest
48
+ brand_path = Appydave::Tools::Dam::Config.brand_path(brand_key)
49
+ manifest_path = File.join(brand_path, 'projects.json')
50
+
51
+ unless File.exist?(manifest_path)
52
+ puts "❌ Manifest not found: #{manifest_path}"
53
+ puts " Run: dam manifest #{brand_key}"
54
+ puts " Then retry: dam s3-scan #{brand_key}"
55
+ exit 1
56
+ end
57
+
58
+ manifest = JSON.parse(File.read(manifest_path), symbolize_names: true)
59
+
60
+ # Identify matched and orphaned S3 projects
61
+ local_project_ids = manifest[:projects].map { |p| p[:id] }
62
+ matched_projects = results.slice(*local_project_ids)
63
+ orphaned_projects = results.reject { |project_id, _| local_project_ids.include?(project_id) }
64
+
65
+ # Merge S3 scan data into manifest for matched projects
66
+ updated_count = 0
67
+ manifest[:projects].each do |project|
68
+ project_id = project[:id]
69
+ s3_data = results[project_id]
70
+ next unless s3_data
71
+
72
+ project[:storage][:s3] = s3_data
73
+ updated_count += 1
74
+ end
75
+
76
+ # Update timestamp and note
77
+ manifest[:config][:last_updated] = Time.now.utc.iso8601
78
+ manifest[:config][:note] = 'Auto-generated manifest with S3 scan data. Regenerate with: dam s3-scan'
79
+
80
+ # Write updated manifest
81
+ File.write(manifest_path, JSON.pretty_generate(manifest))
82
+
83
+ # Add local sync status to matched projects
84
+ Appydave::Tools::Dam::LocalSyncStatus.enrich!(matched_projects, brand_key)
85
+
86
+ # Display table
87
+ display_table(matched_projects, orphaned_projects, bucket, prefix, region)
88
+
89
+ # Summary
90
+ total_manifest_projects = manifest[:projects].size
91
+ missing_count = total_manifest_projects - matched_projects.size
92
+
93
+ puts ''
94
+ puts 'ℹ️ Summary:'
95
+ puts " • Updated #{updated_count} projects in manifest"
96
+ puts " • #{missing_count} local project(s) not yet uploaded to S3" if missing_count.positive?
97
+ puts " • Manifest: #{manifest_path}"
98
+ puts ''
99
+ end
100
+
101
+ # Scan all brands' S3 buckets
102
+ def scan_all
103
+ Appydave::Tools::Configuration::Config.configure
104
+ brands_config = Appydave::Tools::Configuration::Config.brands
105
+
106
+ results = []
107
+ brands_config.brands.each do |brand_info|
108
+ brand_key = brand_info.key
109
+ puts ''
110
+ puts '=' * 60
111
+
112
+ begin
113
+ scan_single(brand_key)
114
+ results << { brand: brand_key, success: true }
115
+ rescue StandardError => e
116
+ puts "❌ Failed to scan #{brand_key}: #{e.message}"
117
+ results << { brand: brand_key, success: false, error: e.message }
118
+ end
119
+ end
120
+
121
+ puts ''
122
+ puts '=' * 60
123
+ puts '📋 Summary - S3 Scans:'
124
+ puts ''
125
+
126
+ successful, failed = results.partition { |r| r[:success] }
127
+
128
+ successful.each do |result|
129
+ brand_display = result[:brand].ljust(15)
130
+ puts "✅ #{brand_display} Scanned successfully"
131
+ end
132
+
133
+ failed.each do |result|
134
+ brand_display = result[:brand].ljust(15)
135
+ puts "❌ #{brand_display} #{result[:error]}"
136
+ end
137
+
138
+ puts ''
139
+ puts "Total brands scanned: #{successful.size}/#{results.size}"
140
+ end
141
+
142
+ private
143
+
144
+ # Display S3 scan results in table format
145
+ # rubocop:disable Style/FormatStringToken
146
+ def display_table(matched_projects, orphaned_projects, bucket, prefix, region)
147
+ puts '✅ S3 Projects Report'
148
+ puts ''
149
+ puts 'PROJECT FILES SIZE LOCAL S3 MODIFIED'
150
+ puts '-' * 92
151
+
152
+ # Display matched projects first (sorted alphabetically)
153
+ matched_projects.sort.each do |project_id, data|
154
+ files = data[:file_count].to_s.rjust(5)
155
+ size = Appydave::Tools::Dam::FileHelper.format_size(data[:total_bytes]).rjust(10)
156
+ local_status = Appydave::Tools::Dam::LocalSyncStatus.format(data[:local_status], data[:local_file_count], data[:file_count])
157
+ modified = data[:last_modified] ? Time.parse(data[:last_modified]).strftime('%Y-%m-%d %H:%M') : 'N/A'
158
+
159
+ puts format('%-36s %5s %10s %-9s %s', project_id, files, size, local_status, modified)
160
+ end
161
+
162
+ # Display orphaned projects (sorted alphabetically)
163
+ return if orphaned_projects.empty?
164
+
165
+ puts '-' * 92
166
+ orphaned_projects.sort.each do |project_id, data|
167
+ files = data[:file_count].to_s.rjust(5)
168
+ size = Appydave::Tools::Dam::FileHelper.format_size(data[:total_bytes]).rjust(10)
169
+ local_status = 'N/A'
170
+ modified = data[:last_modified] ? Time.parse(data[:last_modified]).strftime('%Y-%m-%d %H:%M') : 'N/A'
171
+
172
+ puts format('%-36s %5s %10s %-9s %s', project_id, files, size, local_status, modified)
173
+ end
174
+
175
+ puts ''
176
+ folder_word = orphaned_projects.size > 1 ? 'folders' : 'folder'
177
+ puts "⚠️ #{orphaned_projects.size} orphaned #{folder_word} found (no local project)"
178
+
179
+ orphaned_projects.sort.each do |project_id, _data|
180
+ # Build AWS Console URL
181
+ project_prefix = "#{prefix}#{project_id}/"
182
+ console_url = "https://#{region}.console.aws.amazon.com/s3/buckets/#{bucket}?prefix=#{project_prefix}&region=#{region}"
183
+ puts " → #{project_id}"
184
+ puts " #{console_url}"
185
+ end
186
+ end
187
+ # rubocop:enable Style/FormatStringToken
188
+ end
189
+ end
190
+ end
191
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Appydave
4
4
  module Tools
5
- VERSION = '0.76.5'
5
+ VERSION = '0.76.7'
6
6
  end
7
7
  end
@@ -79,6 +79,8 @@ require 'appydave/tools/dam/repo_status'
79
79
  require 'appydave/tools/dam/repo_sync'
80
80
  require 'appydave/tools/dam/repo_push'
81
81
  require 'appydave/tools/dam/local_sync_status'
82
+ require 'appydave/tools/dam/s3_scan_command'
83
+ require 'appydave/tools/dam/s3_arg_parser'
82
84
 
83
85
  require 'appydave/tools/jump/path_validator'
84
86
  require 'appydave/tools/jump/location'
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appydave-tools",
3
- "version": "0.76.5",
3
+ "version": "0.76.7",
4
4
  "description": "AppyDave YouTube Automation Tools",
5
5
  "scripts": {
6
6
  "release": "semantic-release"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: appydave-tools
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.76.5
4
+ version: 0.76.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Cruwys
@@ -361,7 +361,9 @@ files:
361
361
  - lib/appydave/tools/dam/repo_push.rb
362
362
  - lib/appydave/tools/dam/repo_status.rb
363
363
  - lib/appydave/tools/dam/repo_sync.rb
364
+ - lib/appydave/tools/dam/s3_arg_parser.rb
364
365
  - lib/appydave/tools/dam/s3_operations.rb
366
+ - lib/appydave/tools/dam/s3_scan_command.rb
365
367
  - lib/appydave/tools/dam/s3_scanner.rb
366
368
  - lib/appydave/tools/dam/share_operations.rb
367
369
  - lib/appydave/tools/dam/ssd_status.rb