gsc-cli 2.1.0 → 2.2.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 (100) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +410 -414
  3. data/bin/gsc +28067 -5661
  4. data/dist/gsc +29121 -5046
  5. data/lib/gsc/aio_hunter.rb +343 -0
  6. data/lib/gsc/answer_synthesizer.rb +157 -0
  7. data/lib/gsc/api.rb +53 -1
  8. data/lib/gsc/auth.rb +26 -0
  9. data/lib/gsc/brand_segmenter.rb +140 -0
  10. data/lib/gsc/cache_manager.rb +806 -0
  11. data/lib/gsc/cannibalization_analyzer.rb +141 -0
  12. data/lib/gsc/canonical_chains.rb +367 -0
  13. data/lib/gsc/citation_simulator.rb +339 -0
  14. data/lib/gsc/cli/aio_hunter.rb +154 -0
  15. data/lib/gsc/cli/analytics.rb +788 -0
  16. data/lib/gsc/cli/audit.rb +1976 -0
  17. data/lib/gsc/cli/base.rb +384 -0
  18. data/lib/gsc/cli/cache.rb +266 -0
  19. data/lib/gsc/cli/canonical.rb +223 -0
  20. data/lib/gsc/cli/citation_simulator.rb +152 -0
  21. data/lib/gsc/cli/dashboard.rb +354 -0
  22. data/lib/gsc/cli/doctor.rb +129 -0
  23. data/lib/gsc/cli/eeat.rb +125 -0
  24. data/lib/gsc/cli/ga4.rb +852 -0
  25. data/lib/gsc/cli/growth.rb +650 -0
  26. data/lib/gsc/cli/hreflang.rb +164 -0
  27. data/lib/gsc/cli/image_seo.rb +162 -0
  28. data/lib/gsc/cli/indexing.rb +458 -0
  29. data/lib/gsc/cli/intent_shift.rb +125 -0
  30. data/lib/gsc/cli/keyword_value.rb +134 -0
  31. data/lib/gsc/cli/keywords.rb +795 -0
  32. data/lib/gsc/cli/landing_roi.rb +308 -0
  33. data/lib/gsc/cli/low_ctr.rb +213 -0
  34. data/lib/gsc/cli/mobile_parity.rb +150 -0
  35. data/lib/gsc/cli/report.rb +100 -0
  36. data/lib/gsc/cli/rich_results.rb +172 -0
  37. data/lib/gsc/cli/schema_generate.rb +149 -0
  38. data/lib/gsc/cli/seasonal.rb +232 -0
  39. data/lib/gsc/cli/security.rb +153 -0
  40. data/lib/gsc/cli/setup.rb +1291 -0
  41. data/lib/gsc/cli/sitemap_tree.rb +143 -0
  42. data/lib/gsc/cli/skill_pack.rb +62 -0
  43. data/lib/gsc/cli/soft_404.rb +199 -0
  44. data/lib/gsc/cli/sparkline.rb +227 -0
  45. data/lib/gsc/cli/watchdog.rb +150 -0
  46. data/lib/gsc/cli/zombie_purger.rb +208 -0
  47. data/lib/gsc/cli.rb +707 -5265
  48. data/lib/gsc/cli_advanced.rb +987 -44
  49. data/lib/gsc/client.rb +17 -2
  50. data/lib/gsc/color.rb +16 -1
  51. data/lib/gsc/command_registry.rb +47 -9
  52. data/lib/gsc/config.rb +2 -2
  53. data/lib/gsc/ctr_curve.rb +115 -0
  54. data/lib/gsc/decay_predictor.rb +322 -0
  55. data/lib/gsc/doctor.rb +434 -0
  56. data/lib/gsc/eeat_auditor.rb +428 -0
  57. data/lib/gsc/entity_auditor.rb +229 -0
  58. data/lib/gsc/firewall_scanner.rb +733 -0
  59. data/lib/gsc/geo_auditor.rb +368 -0
  60. data/lib/gsc/google_trends.rb +8 -1
  61. data/lib/gsc/heading_validator.rb +283 -0
  62. data/lib/gsc/hreflang_validator.rb +412 -0
  63. data/lib/gsc/image_seo.rb +286 -0
  64. data/lib/gsc/indexing_queue.rb +179 -0
  65. data/lib/gsc/indexnow.rb +93 -0
  66. data/lib/gsc/intent_shift.rb +188 -0
  67. data/lib/gsc/internal_links.rb +153 -36
  68. data/lib/gsc/keyword_value.rb +191 -0
  69. data/lib/gsc/landing_roi.rb +195 -0
  70. data/lib/gsc/llms_generator.rb +343 -22
  71. data/lib/gsc/low_ctr_rewriter.rb +408 -0
  72. data/lib/gsc/mobile_parity.rb +222 -0
  73. data/lib/gsc/network_tracer.rb +8 -1
  74. data/lib/gsc/page_analyzer.rb +47 -7
  75. data/lib/gsc/prompts.rb +38 -29
  76. data/lib/gsc/questions_harvester.rb +178 -0
  77. data/lib/gsc/report_generator.rb +461 -0
  78. data/lib/gsc/rich_results.rb +388 -0
  79. data/lib/gsc/robots_checker.rb +46 -15
  80. data/lib/gsc/schema_generator.rb +788 -0
  81. data/lib/gsc/schema_validator.rb +36 -38
  82. data/lib/gsc/seasonal_predictor.rb +381 -0
  83. data/lib/gsc/security_scanner.rb +496 -0
  84. data/lib/gsc/serp_feature_detector.rb +359 -0
  85. data/lib/gsc/serp_preview.rb +108 -22
  86. data/lib/gsc/site_crawler.rb +113 -21
  87. data/lib/gsc/sitemap_loader.rb +15 -4
  88. data/lib/gsc/sitemap_tree.rb +301 -0
  89. data/lib/gsc/skill_pack.rb +195 -0
  90. data/lib/gsc/soft_404_analyzer.rb +385 -0
  91. data/lib/gsc/sparkline.rb +171 -0
  92. data/lib/gsc/speed_correlator.rb +416 -0
  93. data/lib/gsc/striking_playbook.rb +190 -0
  94. data/lib/gsc/title_optimizer.rb +420 -0
  95. data/lib/gsc/vault.rb +260 -0
  96. data/lib/gsc/version.rb +1 -1
  97. data/lib/gsc/watchdog.rb +235 -0
  98. data/lib/gsc/zombie_purger.rb +366 -0
  99. data/lib/gsc.rb +118 -0
  100. metadata +75 -1
@@ -0,0 +1,164 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ require 'json'
5
+ require_relative 'base'
6
+ require_relative '../hreflang_validator'
7
+ require_relative '../color'
8
+
9
+ module GSC
10
+ class CLI
11
+ module Hreflang
12
+ module_function
13
+
14
+ def run(command, target, extra, options, api = nil, site_url = nil, hostname = nil)
15
+ target_url = target || (hostname ? "https://#{hostname}" : nil)
16
+ unless target_url
17
+ if File.exist?('public/index.html')
18
+ target_url = 'public/index.html'
19
+ elsif File.exist?('index.html')
20
+ target_url = 'index.html'
21
+ else
22
+ msg = "Please provide a target URL or HTML file: gsc hreflang-check <url|file>"
23
+ if options[:json]
24
+ puts JSON.pretty_generate({ error: msg })
25
+ else
26
+ puts Color.red("❌ Error: #{msg}")
27
+ end
28
+ return
29
+ end
30
+ end
31
+
32
+ puts "🌐 Auditing international hreflang tags & bidirectional reciprocity for #{Color.cyan(target_url)}..." unless options[:json]
33
+
34
+ res = GSC::HreflangValidator.audit(target_url, options)
35
+
36
+ if options[:json]
37
+ puts JSON.pretty_generate(res)
38
+ return
39
+ end
40
+
41
+ render_terminal(res)
42
+
43
+ if options[:csv]
44
+ export_csv(res, options[:csv])
45
+ end
46
+ end
47
+
48
+ def render_terminal(res)
49
+ puts "\n#{Color.bold("═══════════════════════════════════════════════════════════════════════════")}"
50
+ puts "#{Color.bold("🌐 INTERNATIONAL HREFLANG RECIPROCITY & ISO CODE AUDITOR")}"
51
+ puts "#{Color.bold("═══════════════════════════════════════════════════════════════════════════")}"
52
+
53
+ score = res[:health_score]
54
+ grade_color = case res[:grade]
55
+ when 'A+', 'A' then Color.green(res[:grade])
56
+ when 'B' then Color.cyan(res[:grade])
57
+ when 'C' then Color.yellow(res[:grade])
58
+ else Color.red(res[:grade])
59
+ end
60
+
61
+ puts " • Hreflang Health Score: [ #{Color.bold(grade_color)} ] #{Color.bold("#{score}/100")}"
62
+ puts " • Total Alternate Links Found: #{Color.bold(res[:total_tags].to_s)} declarations"
63
+ puts " • Target URL / Source: #{res[:url]}"
64
+ puts "#{Color.bold("───────────────────────────────────────────────────────────────────────────")}"
65
+
66
+ self_badge = res[:self_reference] ? Color.green("✅ Present") : Color.red("❌ Missing (Self-reference is required)")
67
+ x_badge = res[:has_x_default] ? Color.green("✅ Present") : Color.yellow("⚠️ Missing (Recommended fallback)")
68
+
69
+ puts " Core International Specifications:"
70
+ puts " 🔁 Self-Referencing Tag: #{self_badge}"
71
+ puts " 🌍 x-default Global Fallback: #{x_badge}"
72
+ puts " ⇄ Confirmed Reciprocal Links: #{Color.bold(res[:reciprocal_count].to_s)}"
73
+ puts " ⇥ Unreciprocated Return Links: #{res[:unreciprocated_count] > 0 ? Color.red(res[:unreciprocated_count].to_s) : Color.green("0")}"
74
+ puts " ⚠️ Invalid Language/Region Codes: #{res[:invalid_code_count] > 0 ? Color.red(res[:invalid_code_count].to_s) : Color.green("0")}"
75
+ puts "#{Color.bold("───────────────────────────────────────────────────────────────────────────")}\n"
76
+
77
+ if res[:tags].empty?
78
+ puts Color.yellow("⚠️ No hreflang alternate tags discovered in target document or response headers.")
79
+ puts " Add <link rel=\"alternate\" hreflang=\"...\" href=\"...\" /> tags to designate localized variants.\n"
80
+ return
81
+ end
82
+
83
+ puts "#{Color.bold("HREFLANG CLUSTER DECLARATIONS:")}\n\n"
84
+ printf " %-12s %-40s %-16s %s\n", "Hreflang", "Destination URL", "Status", "Issues / Notes"
85
+ puts " ---------------------------------------------------------------------------------------"
86
+
87
+ res[:tags].each do |tag|
88
+ dest = tag[:href].length > 38 ? "#{tag[:href][0..35]}..." : tag[:href]
89
+
90
+ status_str = case tag[:status]
91
+ when :self_referential then Color.cyan("🔁 Self-Ref")
92
+ when :confirmed then Color.green("⇄ Confirmed")
93
+ when :missing_return then Color.red("⇥ No Return")
94
+ when :http_error then Color.red("❌ HTTP Error")
95
+ when :unreachable then Color.gray("⚠️ Unreachable")
96
+ else Color.gray("— Pending")
97
+ end
98
+
99
+ issue_notes = if tag[:correction]
100
+ Color.yellow("Fix: #{tag[:correction]}")
101
+ elsif tag[:issues].empty?
102
+ Color.green("Valid Code")
103
+ else
104
+ tag[:issues].map { |i| format_issue(i) }.join(', ')
105
+ end
106
+
107
+ printf " %-12s %-40s %-16s %s\n", tag[:hreflang], dest, status_str, issue_notes
108
+ end
109
+
110
+ puts "\n#{Color.bold("───────────────────────────────────────────────────────────────────────────")}"
111
+
112
+ if res[:prescriptions].any?
113
+ puts "#{Color.bold("PRIORITIZED INTERNATIONAL SEO PRESCRIPTIONS:")}"
114
+ res[:prescriptions].each_with_index do |p, idx|
115
+ puts " #{idx + 1}. 💡 #{p}"
116
+ end
117
+ puts
118
+ end
119
+
120
+ if res[:cluster_snippets][:html] && !res[:cluster_snippets][:html].empty?
121
+ puts "#{Color.bold("1-CLICK STANDARDIZED HTML CLUSTER CODE:")}"
122
+ puts "```html"
123
+ puts res[:cluster_snippets][:html]
124
+ puts "```\n"
125
+
126
+ puts "#{Color.bold("1-CLICK GOOGLE XML SITEMAP <XHTML:LINK> CODE:")}"
127
+ puts "```xml"
128
+ puts res[:cluster_snippets][:sitemap_xml]
129
+ puts "```\n"
130
+ end
131
+
132
+ puts "#{Color.bold("═══════════════════════════════════════════════════════════════════════════")}\n"
133
+ end
134
+
135
+ def format_issue(issue)
136
+ case issue
137
+ when :invalid_language then Color.red("Invalid Language")
138
+ when :invalid_region then Color.red("Invalid Region")
139
+ when :common_mistake then Color.yellow("Common Code Error")
140
+ when :uppercase_language then Color.yellow("Uppercase Lang (Use Lowercase)")
141
+ when :missing_reciprocal_tag then Color.red("Missing Return Tag")
142
+ when :invalid_script then Color.red("Invalid Script")
143
+ when :lowercase_region then Color.yellow("Lowercase Region (Use Uppercase)")
144
+ else issue.to_s
145
+ end
146
+ end
147
+
148
+ def export_csv(res, file_path)
149
+ headers = %w[Hreflang Destination Status Source Issues]
150
+ rows = res[:tags].map do |t|
151
+ [
152
+ t[:hreflang],
153
+ t[:href],
154
+ t[:status].to_s,
155
+ t[:source].to_s,
156
+ t[:issues].join('|')
157
+ ]
158
+ end
159
+ Base.write_csv(file_path, headers, rows)
160
+ puts Color.cyan("📁 Successfully exported hreflang audit to #{file_path}")
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,162 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ require 'json'
5
+ require_relative 'base'
6
+ require_relative '../image_seo'
7
+ require_relative '../color'
8
+
9
+ module GSC
10
+ class CLI
11
+ module ImageSeo
12
+ module_function
13
+
14
+ def run(command, target, extra, options, api = nil, site_url = nil, hostname = nil)
15
+ target_url = target || (hostname ? "https://#{hostname}" : nil)
16
+ unless target_url
17
+ if File.exist?('public/index.html')
18
+ target_url = 'public/index.html'
19
+ elsif File.exist?('index.html')
20
+ target_url = 'index.html'
21
+ else
22
+ msg = "Please provide a target URL or HTML file: gsc image-seo <url|file>"
23
+ if options[:json]
24
+ puts JSON.pretty_generate({ error: msg })
25
+ else
26
+ puts Color.red("❌ Error: #{msg}")
27
+ end
28
+ return
29
+ end
30
+ end
31
+
32
+ puts "🖼️ Auditing image SEO, dimensions & next-gen formats for #{Color.cyan(target_url)}..." unless options[:json]
33
+
34
+ res = GSC::ImageSeo.audit(target_url, options)
35
+
36
+ if options[:json]
37
+ puts JSON.pretty_generate(res)
38
+ return
39
+ end
40
+
41
+ render_terminal(res)
42
+
43
+ if options[:csv]
44
+ export_csv(res, options[:csv])
45
+ end
46
+ end
47
+
48
+ def render_terminal(res)
49
+ puts "\n#{Color.bold("═══════════════════════════════════════════════════════════════════════════")}"
50
+ puts "#{Color.bold("🖼️ IMAGE SEO & NEXT-GEN FORMAT AUDITOR (WebP • AVIF • CLS • Alt)")}"
51
+ puts "#{Color.bold("═══════════════════════════════════════════════════════════════════════════")}"
52
+
53
+ score = res[:health_score]
54
+ grade_color = case res[:grade]
55
+ when 'A+', 'A' then Color.green(res[:grade])
56
+ when 'B' then Color.cyan(res[:grade])
57
+ when 'C' then Color.yellow(res[:grade])
58
+ else Color.red(res[:grade])
59
+ end
60
+
61
+ puts " • Image SEO Health Score: [ #{Color.bold(grade_color)} ] #{Color.bold("#{score}/100")}"
62
+ puts " • Total Images Discovered: #{Color.bold(res[:total_images].to_s)} images"
63
+ puts " • Target URL / Source: #{res[:url]}"
64
+ puts "#{Color.bold("───────────────────────────────────────────────────────────────────────────")}"
65
+
66
+ m = res[:metrics]
67
+ puts " Image Compliance Dimensions:"
68
+ puts " 🏷️ Alt Attribute Coverage: #{Color.bold("#{m[:alt_coverage_pct]}%")}"
69
+ puts " 📐 Explicit Width & Height (CLS): #{Color.bold("#{m[:dimension_coverage_pct]}%")}"
70
+ puts " ⚡ Next-Gen Formats (WebP/AVIF): #{Color.bold("#{m[:modern_format_pct]}%")}"
71
+ puts " ⏳ Proper Lazy-Loading / LCP: #{Color.bold("#{m[:lazy_loading_pct]}%")}"
72
+ puts "#{Color.bold("───────────────────────────────────────────────────────────────────────────")}\n"
73
+
74
+ if res[:images].empty?
75
+ puts Color.green("🎉 No image tags found on target page.")
76
+ return
77
+ end
78
+
79
+ puts "#{Color.bold("IMAGE ASSET AUDIT BREAKDOWN (First #{res[:images].size} of #{res[:total_images]} images):")}\n\n"
80
+ printf " %-4s %-32s %-8s %-12s %-10s %s\n", "#", "Source File", "Format", "Dimensions", "Alt Status", "Issues"
81
+ puts " ---------------------------------------------------------------------------------------"
82
+
83
+ res[:images].each do |img|
84
+ filename = File.basename(img[:src].split('?').first)
85
+ short_file = filename.length > 30 ? "#{filename[0..27]}..." : filename
86
+
87
+ dim_str = (img[:width] && img[:height]) ? "#{img[:width]}x#{img[:height]}" : Color.red("MISSING")
88
+
89
+ alt_status = if img[:alt].nil?
90
+ Color.red("MISSING")
91
+ elsif img[:alt].empty?
92
+ img[:is_decorative] ? Color.gray("DECORATIVE") : Color.yellow("EMPTY")
93
+ else
94
+ Color.green("OK")
95
+ end
96
+
97
+ issues_summary = if img[:issues].empty?
98
+ Color.green("✅ Clean")
99
+ else
100
+ img[:issues].map { |i| format_issue(i) }.join(', ')
101
+ end
102
+
103
+ printf " %-4s %-32s %-8s %-12s %-10s %s\n", img[:index], short_file, img[:format].upcase, dim_str, alt_status, issues_summary
104
+ end
105
+
106
+ puts "\n#{Color.bold("───────────────────────────────────────────────────────────────────────────")}"
107
+
108
+ if res[:prescriptions].any?
109
+ puts "#{Color.bold("PRIORITIZED IMAGE SEO PRESCRIPTIONS:")}"
110
+ res[:prescriptions].each_with_index do |p, idx|
111
+ puts " #{idx + 1}. 💡 #{p}"
112
+ end
113
+ puts
114
+ end
115
+
116
+ # Code Snippet Preview for the first flagged image
117
+ sample_img = res[:images].find { |i| i[:issues].any? } || res[:images].first
118
+ if sample_img
119
+ puts "#{Color.bold("1-CLICK MODERN <PICTURE> CODE FIX (For #{File.basename(sample_img[:src])}):")}"
120
+ puts "```html"
121
+ puts sample_img[:picture_tag_snippet]
122
+ puts "```\n"
123
+ end
124
+
125
+ puts "#{Color.bold("═══════════════════════════════════════════════════════════════════════════")}\n"
126
+ end
127
+
128
+ def format_issue(issue)
129
+ case issue
130
+ when :missing_alt then Color.red("Missing Alt")
131
+ when :empty_alt then Color.yellow("Empty Alt")
132
+ when :missing_dimensions then Color.red("No Width/Height (CLS)")
133
+ when :legacy_format then Color.yellow("Legacy Format")
134
+ when :lcp_lazy_loaded then Color.red("Hero Lazy Loaded (LCP Risk)")
135
+ when :hero_missing_priority then Color.yellow("Hero Needs Priority")
136
+ when :missing_lazy then Color.gray("Needs Lazy")
137
+ when :oversized_payload then Color.red("Oversized >200KB")
138
+ else issue.to_s
139
+ end
140
+ end
141
+
142
+ def export_csv(res, file_path)
143
+ headers = %w[Index Source Format Width Height Alt Loading Hero Issues]
144
+ rows = res[:images].map do |img|
145
+ [
146
+ img[:index],
147
+ img[:src],
148
+ img[:format],
149
+ img[:width] || '',
150
+ img[:height] || '',
151
+ img[:alt] || '',
152
+ img[:loading] || '',
153
+ img[:is_hero] ? 'YES' : 'NO',
154
+ img[:issues].join('|')
155
+ ]
156
+ end
157
+ Base.write_csv(file_path, headers, rows)
158
+ puts Color.cyan("📁 Successfully exported image audit to #{file_path}")
159
+ end
160
+ end
161
+ end
162
+ end