gsc-cli 2.0.1 → 2.1.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/AUTH.md +205 -0
- data/FUNDING.md +120 -0
- data/README.md +237 -38
- data/bin/gsc +2028 -197
- data/dist/gsc +167 -5
- data/lib/gsc/backlinks_manager.rb +96 -0
- data/lib/gsc/cli.rb +156 -3
- data/lib/gsc/cli_advanced.rb +570 -0
- data/lib/gsc/config.rb +9 -0
- data/lib/gsc/content_gap.rb +112 -0
- data/lib/gsc/google_suggest.rb +109 -0
- data/lib/gsc/internal_links.rb +132 -0
- data/lib/gsc/llms_generator.rb +104 -0
- data/lib/gsc/network_tracer.rb +86 -0
- data/lib/gsc/open_page_rank.rb +72 -0
- data/lib/gsc/page_comparator.rb +108 -0
- data/lib/gsc/page_speed.rb +110 -0
- data/lib/gsc/robots_checker.rb +83 -0
- data/lib/gsc/schema_validator.rb +122 -0
- data/lib/gsc/serp_preview.rb +66 -0
- data/lib/gsc/version.rb +1 -1
- data/lib/gsc.rb +26 -0
- metadata +33 -6
data/dist/gsc
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
# ==============================================================================
|
|
5
5
|
# Google Search Console & Indexing API CLI Tool (Pure Ruby - Zero Gem Dependencies)
|
|
6
|
-
# Standalone Single-File Distribution (Built from lib/gsc v2.0
|
|
6
|
+
# Standalone Single-File Distribution (Built from lib/gsc v2.1.0)
|
|
7
7
|
# ==============================================================================
|
|
8
8
|
|
|
9
9
|
require 'net/http'
|
|
@@ -20,7 +20,7 @@ require 'stringio'
|
|
|
20
20
|
|
|
21
21
|
# --- version.rb ---
|
|
22
22
|
module GSC
|
|
23
|
-
VERSION = '2.0
|
|
23
|
+
VERSION = '2.1.0'
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
# --- color.rb ---
|
|
@@ -55,6 +55,15 @@ module GSC
|
|
|
55
55
|
CONFIG_DIR = File.expand_path('~/.config/gsc')
|
|
56
56
|
CONFIG_FILE = File.join(CONFIG_DIR, 'config.json')
|
|
57
57
|
|
|
58
|
+
def self.get(key)
|
|
59
|
+
load[key.to_s]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.set(key, value)
|
|
63
|
+
save(key.to_s => value)
|
|
64
|
+
value
|
|
65
|
+
end
|
|
66
|
+
|
|
58
67
|
def self.load
|
|
59
68
|
return {} unless File.exist?(CONFIG_FILE)
|
|
60
69
|
JSON.parse(File.read(CONFIG_FILE))
|
|
@@ -2844,6 +2853,22 @@ opts.on('--save', 'Save keyword research snapshot to ~/.config/gsc/domains/<doma
|
|
|
2844
2853
|
options[:save] = true
|
|
2845
2854
|
end
|
|
2846
2855
|
|
|
2856
|
+
opts.on('--alphabet', 'Run alphabet soup harvest (a-z) for search suggestions') do
|
|
2857
|
+
options[:alphabet] = true
|
|
2858
|
+
end
|
|
2859
|
+
|
|
2860
|
+
opts.on('--numbers', 'Include numbers (0-9) in alphabet soup search suggestions') do
|
|
2861
|
+
options[:numbers] = true
|
|
2862
|
+
end
|
|
2863
|
+
|
|
2864
|
+
opts.on('--strategy STRAT', 'PageSpeed device strategy: mobile or desktop (default: mobile)') do |s|
|
|
2865
|
+
options[:strategy] = s
|
|
2866
|
+
end
|
|
2867
|
+
|
|
2868
|
+
opts.on('--bot NAME', 'User agent bot name for robots.txt testing (default: googlebot)') do |b|
|
|
2869
|
+
options[:bot] = b
|
|
2870
|
+
end
|
|
2871
|
+
|
|
2847
2872
|
|
|
2848
2873
|
opts.on('--delay MS', Integer, 'Delay between sequential requests in ms (default: 120)') do |delay|
|
|
2849
2874
|
options[:delay] = delay
|
|
@@ -2913,6 +2938,72 @@ opts.on('--dry-run', 'Simulate API calls without mutating data') do
|
|
|
2913
2938
|
exit 0 unless options[:in_dashboard]
|
|
2914
2939
|
return
|
|
2915
2940
|
|
|
2941
|
+
when 'suggest', 'autocomplete', 'sug'
|
|
2942
|
+
handle_suggest_command(target, options)
|
|
2943
|
+
exit 0 unless options[:in_dashboard]
|
|
2944
|
+
return if options[:in_dashboard]
|
|
2945
|
+
|
|
2946
|
+
when 'questions', 'paa', 'faqs'
|
|
2947
|
+
handle_questions_command(target, options)
|
|
2948
|
+
exit 0 unless options[:in_dashboard]
|
|
2949
|
+
return if options[:in_dashboard]
|
|
2950
|
+
|
|
2951
|
+
when 'authority', 'opr', 'da', 'domain-authority'
|
|
2952
|
+
handle_authority_command(target, extra, options)
|
|
2953
|
+
exit 0 unless options[:in_dashboard]
|
|
2954
|
+
return if options[:in_dashboard]
|
|
2955
|
+
|
|
2956
|
+
when 'speed', 'vitals', 'pagespeed', 'psi'
|
|
2957
|
+
handle_speed_command(target, options)
|
|
2958
|
+
exit 0 unless options[:in_dashboard]
|
|
2959
|
+
return if options[:in_dashboard]
|
|
2960
|
+
|
|
2961
|
+
when 'compare', 'diff-seo', 'vs'
|
|
2962
|
+
handle_compare_command(target, extra, options)
|
|
2963
|
+
exit 0 unless options[:in_dashboard]
|
|
2964
|
+
return if options[:in_dashboard]
|
|
2965
|
+
|
|
2966
|
+
when 'content-gap', 'gap'
|
|
2967
|
+
handle_content_gap_command(target, extra, options)
|
|
2968
|
+
exit 0 unless options[:in_dashboard]
|
|
2969
|
+
return if options[:in_dashboard]
|
|
2970
|
+
|
|
2971
|
+
when 'internal-links', 'orphans', 'links-audit'
|
|
2972
|
+
handle_internal_links_command(target, options)
|
|
2973
|
+
exit 0 unless options[:in_dashboard]
|
|
2974
|
+
return if options[:in_dashboard]
|
|
2975
|
+
|
|
2976
|
+
when 'schema', 'rich-snippets', 'ld-json'
|
|
2977
|
+
handle_schema_command(target, extra, options)
|
|
2978
|
+
exit 0 unless options[:in_dashboard]
|
|
2979
|
+
return if options[:in_dashboard]
|
|
2980
|
+
|
|
2981
|
+
when 'llms', 'ai-ready'
|
|
2982
|
+
handle_llms_command(target, extra, options)
|
|
2983
|
+
exit 0 unless options[:in_dashboard]
|
|
2984
|
+
return if options[:in_dashboard]
|
|
2985
|
+
|
|
2986
|
+
when 'preview', 'serp-preview', 'social-preview'
|
|
2987
|
+
handle_preview_command(target, options)
|
|
2988
|
+
exit 0 unless options[:in_dashboard]
|
|
2989
|
+
return if options[:in_dashboard]
|
|
2990
|
+
|
|
2991
|
+
when 'trace', 'redirects', 'hops'
|
|
2992
|
+
handle_trace_command(target, options)
|
|
2993
|
+
exit 0 unless options[:in_dashboard]
|
|
2994
|
+
return if options[:in_dashboard]
|
|
2995
|
+
|
|
2996
|
+
when 'robots', 'robots-txt'
|
|
2997
|
+
handle_robots_command(target, extra, options)
|
|
2998
|
+
exit 0 unless options[:in_dashboard]
|
|
2999
|
+
return if options[:in_dashboard]
|
|
3000
|
+
|
|
3001
|
+
when 'backlinks', 'links'
|
|
3002
|
+
handle_backlinks_command(target, extra, options)
|
|
3003
|
+
exit 0 unless options[:in_dashboard]
|
|
3004
|
+
return if options[:in_dashboard]
|
|
3005
|
+
|
|
3006
|
+
|
|
2916
3007
|
when 'trends', 'tr', 'google-trends', 'gtrends', 't'
|
|
2917
3008
|
if target.nil? || target.strip.empty?
|
|
2918
3009
|
# No target query given: pass through to GSC period decay/trends below
|
|
@@ -2983,7 +3074,7 @@ when 'version', '-v', '--version'
|
|
|
2983
3074
|
exit 0 unless options[:in_dashboard]
|
|
2984
3075
|
return if options[:in_dashboard]
|
|
2985
3076
|
when 'config'
|
|
2986
|
-
handle_config_command(target, extra, options)
|
|
3077
|
+
handle_config_command(target, extra, args[3], options)
|
|
2987
3078
|
exit 0 unless options[:in_dashboard]
|
|
2988
3079
|
return if options[:in_dashboard]
|
|
2989
3080
|
when 'connect', 'setup', 'init', 'install'
|
|
@@ -6668,8 +6759,56 @@ def self.handle_skills_command(subcommand, options)
|
|
|
6668
6759
|
puts
|
|
6669
6760
|
end
|
|
6670
6761
|
|
|
6671
|
-
def self.handle_config_command(subcommand, subvalue,
|
|
6762
|
+
def self.handle_config_command(subcommand, subvalue, subextra_or_options = nil, maybe_options = {})
|
|
6763
|
+
if subextra_or_options.is_a?(Hash)
|
|
6764
|
+
options = subextra_or_options
|
|
6765
|
+
subextra = nil
|
|
6766
|
+
else
|
|
6767
|
+
subextra = subextra_or_options
|
|
6768
|
+
options = maybe_options || {}
|
|
6769
|
+
end
|
|
6672
6770
|
case subcommand
|
|
6771
|
+
when 'set'
|
|
6772
|
+
key = subvalue.to_s.strip
|
|
6773
|
+
val = subextra.to_s.strip
|
|
6774
|
+
if key.empty? || val.empty?
|
|
6775
|
+
if options[:json]
|
|
6776
|
+
puts JSON.pretty_generate({ error: 'Usage: gsc config set <key> <value>' })
|
|
6777
|
+
else
|
|
6778
|
+
puts Color.c("❌ Error: Please specify key and value.", Color::RED)
|
|
6779
|
+
puts "Example: gsc config set opr_api_key opr_live_xxxx"
|
|
6780
|
+
puts "Example: gsc config set pagespeed_api_key AIzaSyxxxx"
|
|
6781
|
+
end
|
|
6782
|
+
exit 1
|
|
6783
|
+
end
|
|
6784
|
+
|
|
6785
|
+
Config.set(key, val)
|
|
6786
|
+
if key == 'opr_api_key'
|
|
6787
|
+
Config.set('openpagerank_api_key', val)
|
|
6788
|
+
elsif key == 'openpagerank_api_key'
|
|
6789
|
+
Config.set('opr_api_key', val)
|
|
6790
|
+
end
|
|
6791
|
+
|
|
6792
|
+
masked = val.length > 8 ? "#{val[0..7]}...#{val[-4..-1]}" : "***"
|
|
6793
|
+
if options[:json]
|
|
6794
|
+
puts JSON.pretty_generate({ status: 'ok', key: key, value: masked, configFile: Config::CONFIG_FILE })
|
|
6795
|
+
else
|
|
6796
|
+
puts BANNER
|
|
6797
|
+
puts Color.c("✅ Saved configuration: #{Color.c(key, Color::CYAN)} = #{Color.c(masked, Color::GREEN)}", Color::GREEN, Color::BOLD)
|
|
6798
|
+
puts Color.c(" 📁 Stored in #{Config::CONFIG_FILE}", Color::GRAY)
|
|
6799
|
+
end
|
|
6800
|
+
return
|
|
6801
|
+
|
|
6802
|
+
when 'get'
|
|
6803
|
+
key = subvalue.to_s.strip
|
|
6804
|
+
val = Config.get(key)
|
|
6805
|
+
if options[:json]
|
|
6806
|
+
puts JSON.pretty_generate({ key: key, value: val })
|
|
6807
|
+
else
|
|
6808
|
+
puts val || "(not set)"
|
|
6809
|
+
end
|
|
6810
|
+
return
|
|
6811
|
+
|
|
6673
6812
|
when 'set-domain', 'domain'
|
|
6674
6813
|
if subvalue.nil? || subvalue.empty?
|
|
6675
6814
|
if options[:json]
|
|
@@ -6775,7 +6914,28 @@ def self.handle_skills_command(subcommand, options)
|
|
|
6775
6914
|
else
|
|
6776
6915
|
puts " Key Location: #{Color.c('Not found', Color::RED)}"
|
|
6777
6916
|
end
|
|
6778
|
-
|
|
6917
|
+
opr = cfg['opr_api_key'] || cfg['openpagerank_api_key']
|
|
6918
|
+
if opr
|
|
6919
|
+
m_opr = opr.length > 8 ? "#{opr[0..7]}...#{opr[-4..-1]}" : "***"
|
|
6920
|
+
puts " OpenPageRank: #{Color.c(m_opr, Color::GREEN)} (Active)"
|
|
6921
|
+
else
|
|
6922
|
+
puts " OpenPageRank: #{Color.c('(not set - run: gsc config set opr_api_key <key>)', Color::GRAY)}"
|
|
6923
|
+
end
|
|
6924
|
+
|
|
6925
|
+
ps = cfg['pagespeed_api_key']
|
|
6926
|
+
if ps
|
|
6927
|
+
m_ps = ps.length > 8 ? "#{ps[0..7]}...#{ps[-4..-1]}" : "***"
|
|
6928
|
+
puts " PageSpeed API: #{Color.c(m_ps, Color::GREEN)} (Active)"
|
|
6929
|
+
end
|
|
6930
|
+
|
|
6931
|
+
ke = cfg['keywords_everywhere_api_key']
|
|
6932
|
+
if ke
|
|
6933
|
+
m_ke = ke.length > 8 ? "#{ke[0..7]}...#{ke[-4..-1]}" : "***"
|
|
6934
|
+
puts " Keywords Evr: #{Color.c(m_ke, Color::GREEN)} (Active)"
|
|
6935
|
+
end
|
|
6936
|
+
|
|
6937
|
+
puts "
|
|
6938
|
+
#{Color::BOLD}Linked GA4 Properties per Domain:#{Color::RESET}"
|
|
6779
6939
|
ga4_props = cfg['ga4_properties'] || {}
|
|
6780
6940
|
if ga4_props.empty?
|
|
6781
6941
|
puts " #{Color.c('(None linked yet. Use: gsc config set-ga4 <id> -d <domain>)', Color::GRAY)}"
|
|
@@ -6789,6 +6949,8 @@ def self.handle_skills_command(subcommand, options)
|
|
|
6789
6949
|
puts "#{Color::BOLD}Configuration shortcuts:#{Color::RESET}"
|
|
6790
6950
|
puts " gsc connect Interactive setup wizard (drag & drop key)"
|
|
6791
6951
|
puts " gsc use <domain> Set active default domain"
|
|
6952
|
+
puts " gsc config set <key> <val> Set API key (e.g. opr_api_key, pagespeed_api_key)"
|
|
6953
|
+
puts " gsc config get <key> Read a stored configuration value"
|
|
6792
6954
|
puts " gsc config set-ga4 <id> Link a GA4 Property ID to domain"
|
|
6793
6955
|
puts " gsc open Open config directory in Finder"
|
|
6794
6956
|
puts " gsc where Show install path and paths summary"
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'csv'
|
|
5
|
+
require 'fileutils'
|
|
6
|
+
require 'json'
|
|
7
|
+
|
|
8
|
+
module GSC
|
|
9
|
+
class BacklinksManager
|
|
10
|
+
attr_reader :domain, :storage_file
|
|
11
|
+
|
|
12
|
+
def initialize(domain)
|
|
13
|
+
@domain = clean_domain(domain)
|
|
14
|
+
@domain_dir = File.join(Dir.home, '.config', 'gsc', 'domains', @domain)
|
|
15
|
+
FileUtils.mkdir_p(@domain_dir)
|
|
16
|
+
@storage_file = File.join(@domain_dir, 'backlinks.json')
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def load_data
|
|
20
|
+
return { 'sources' => [], 'targets' => [], 'updated_at' => nil } unless File.exist?(@storage_file)
|
|
21
|
+
|
|
22
|
+
JSON.parse(File.read(@storage_file, encoding: 'UTF-8'))
|
|
23
|
+
rescue StandardError
|
|
24
|
+
{ 'sources' => [], 'targets' => [], 'updated_at' => nil }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def save_data(data)
|
|
28
|
+
data['updated_at'] = Time.now.utc.iso8601
|
|
29
|
+
File.write(@storage_file, JSON.pretty_generate(data))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def import_csv(content)
|
|
33
|
+
data = load_data
|
|
34
|
+
lines = content.to_s.strip.lines
|
|
35
|
+
|
|
36
|
+
return { error: 'Empty content' } if lines.empty?
|
|
37
|
+
|
|
38
|
+
# Detect header
|
|
39
|
+
header = lines.first.downcase
|
|
40
|
+
if header.include?('top linking sites') || header.include?('root domain')
|
|
41
|
+
# Sources CSV
|
|
42
|
+
sources = []
|
|
43
|
+
CSV.parse(content, headers: true, skip_blanks: true) do |row|
|
|
44
|
+
domain_name = row['Top linking sites'] || row['Site'] || row[0]
|
|
45
|
+
links_count = (row['Target pages'] || row['Links'] || row[1] || '1').to_s.gsub(',', '').to_i
|
|
46
|
+
sources << { 'domain' => domain_name.to_s.strip, 'links_count' => links_count } if domain_name
|
|
47
|
+
end
|
|
48
|
+
data['sources'] = sources
|
|
49
|
+
else
|
|
50
|
+
# Target pages CSV
|
|
51
|
+
targets = []
|
|
52
|
+
CSV.parse(content, headers: true, skip_blanks: true) do |row|
|
|
53
|
+
target_url = row['Target page'] || row['Page'] || row[0]
|
|
54
|
+
incoming = (row['Incoming links'] || row['Links'] || row[1] || '1').to_s.gsub(',', '').to_i
|
|
55
|
+
targets << { 'target_url' => target_url.to_s.strip, 'incoming_count' => incoming } if target_url
|
|
56
|
+
end
|
|
57
|
+
data['targets'] = targets
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
save_data(data)
|
|
61
|
+
{
|
|
62
|
+
status: 'success',
|
|
63
|
+
sources_count: data['sources'].length,
|
|
64
|
+
targets_count: data['targets'].length,
|
|
65
|
+
updated_at: data['updated_at']
|
|
66
|
+
}
|
|
67
|
+
rescue StandardError => e
|
|
68
|
+
{ error: "Failed to parse CSV: #{e.message}" }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def summary(target_page: nil)
|
|
72
|
+
data = load_data
|
|
73
|
+
sources = data['sources'] || []
|
|
74
|
+
targets = data['targets'] || []
|
|
75
|
+
|
|
76
|
+
filtered_targets = target_page ? targets.select { |t| t['target_url'].include?(target_page) } : targets
|
|
77
|
+
|
|
78
|
+
{
|
|
79
|
+
domain: @domain,
|
|
80
|
+
total_referring_domains: sources.length,
|
|
81
|
+
total_external_links: sources.sum { |s| s['links_count'] || 0 },
|
|
82
|
+
top_referring_domains: sources.sort_by { |s| -(s['links_count'] || 0) }.first(15),
|
|
83
|
+
top_target_pages: filtered_targets.sort_by { |t| -(t['incoming_count'] || 0) }.first(15),
|
|
84
|
+
updated_at: data['updated_at']
|
|
85
|
+
}
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def clean_domain(input)
|
|
91
|
+
d = input.to_s.strip.downcase
|
|
92
|
+
d = d.sub(%r{^https?://}, '').sub(%r{/.*$}, '').sub(/^www\./, '')
|
|
93
|
+
d
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
data/lib/gsc/cli.rb
CHANGED
|
@@ -161,6 +161,22 @@ opts.on('--save', 'Save keyword research snapshot to ~/.config/gsc/domains/<doma
|
|
|
161
161
|
options[:save] = true
|
|
162
162
|
end
|
|
163
163
|
|
|
164
|
+
opts.on('--alphabet', 'Run alphabet soup harvest (a-z) for search suggestions') do
|
|
165
|
+
options[:alphabet] = true
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
opts.on('--numbers', 'Include numbers (0-9) in alphabet soup search suggestions') do
|
|
169
|
+
options[:numbers] = true
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
opts.on('--strategy STRAT', 'PageSpeed device strategy: mobile or desktop (default: mobile)') do |s|
|
|
173
|
+
options[:strategy] = s
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
opts.on('--bot NAME', 'User agent bot name for robots.txt testing (default: googlebot)') do |b|
|
|
177
|
+
options[:bot] = b
|
|
178
|
+
end
|
|
179
|
+
|
|
164
180
|
|
|
165
181
|
opts.on('--delay MS', Integer, 'Delay between sequential requests in ms (default: 120)') do |delay|
|
|
166
182
|
options[:delay] = delay
|
|
@@ -230,6 +246,72 @@ opts.on('--dry-run', 'Simulate API calls without mutating data') do
|
|
|
230
246
|
exit 0 unless options[:in_dashboard]
|
|
231
247
|
return
|
|
232
248
|
|
|
249
|
+
when 'suggest', 'autocomplete', 'sug'
|
|
250
|
+
handle_suggest_command(target, options)
|
|
251
|
+
exit 0 unless options[:in_dashboard]
|
|
252
|
+
return if options[:in_dashboard]
|
|
253
|
+
|
|
254
|
+
when 'questions', 'paa', 'faqs'
|
|
255
|
+
handle_questions_command(target, options)
|
|
256
|
+
exit 0 unless options[:in_dashboard]
|
|
257
|
+
return if options[:in_dashboard]
|
|
258
|
+
|
|
259
|
+
when 'authority', 'opr', 'da', 'domain-authority'
|
|
260
|
+
handle_authority_command(target, extra, options)
|
|
261
|
+
exit 0 unless options[:in_dashboard]
|
|
262
|
+
return if options[:in_dashboard]
|
|
263
|
+
|
|
264
|
+
when 'speed', 'vitals', 'pagespeed', 'psi'
|
|
265
|
+
handle_speed_command(target, options)
|
|
266
|
+
exit 0 unless options[:in_dashboard]
|
|
267
|
+
return if options[:in_dashboard]
|
|
268
|
+
|
|
269
|
+
when 'compare', 'diff-seo', 'vs'
|
|
270
|
+
handle_compare_command(target, extra, options)
|
|
271
|
+
exit 0 unless options[:in_dashboard]
|
|
272
|
+
return if options[:in_dashboard]
|
|
273
|
+
|
|
274
|
+
when 'content-gap', 'gap'
|
|
275
|
+
handle_content_gap_command(target, extra, options)
|
|
276
|
+
exit 0 unless options[:in_dashboard]
|
|
277
|
+
return if options[:in_dashboard]
|
|
278
|
+
|
|
279
|
+
when 'internal-links', 'orphans', 'links-audit'
|
|
280
|
+
handle_internal_links_command(target, options)
|
|
281
|
+
exit 0 unless options[:in_dashboard]
|
|
282
|
+
return if options[:in_dashboard]
|
|
283
|
+
|
|
284
|
+
when 'schema', 'rich-snippets', 'ld-json'
|
|
285
|
+
handle_schema_command(target, extra, options)
|
|
286
|
+
exit 0 unless options[:in_dashboard]
|
|
287
|
+
return if options[:in_dashboard]
|
|
288
|
+
|
|
289
|
+
when 'llms', 'ai-ready'
|
|
290
|
+
handle_llms_command(target, extra, options)
|
|
291
|
+
exit 0 unless options[:in_dashboard]
|
|
292
|
+
return if options[:in_dashboard]
|
|
293
|
+
|
|
294
|
+
when 'preview', 'serp-preview', 'social-preview'
|
|
295
|
+
handle_preview_command(target, options)
|
|
296
|
+
exit 0 unless options[:in_dashboard]
|
|
297
|
+
return if options[:in_dashboard]
|
|
298
|
+
|
|
299
|
+
when 'trace', 'redirects', 'hops'
|
|
300
|
+
handle_trace_command(target, options)
|
|
301
|
+
exit 0 unless options[:in_dashboard]
|
|
302
|
+
return if options[:in_dashboard]
|
|
303
|
+
|
|
304
|
+
when 'robots', 'robots-txt'
|
|
305
|
+
handle_robots_command(target, extra, options)
|
|
306
|
+
exit 0 unless options[:in_dashboard]
|
|
307
|
+
return if options[:in_dashboard]
|
|
308
|
+
|
|
309
|
+
when 'backlinks', 'links'
|
|
310
|
+
handle_backlinks_command(target, extra, options)
|
|
311
|
+
exit 0 unless options[:in_dashboard]
|
|
312
|
+
return if options[:in_dashboard]
|
|
313
|
+
|
|
314
|
+
|
|
233
315
|
when 'trends', 'tr', 'google-trends', 'gtrends', 't'
|
|
234
316
|
if target.nil? || target.strip.empty?
|
|
235
317
|
# No target query given: pass through to GSC period decay/trends below
|
|
@@ -300,7 +382,7 @@ when 'version', '-v', '--version'
|
|
|
300
382
|
exit 0 unless options[:in_dashboard]
|
|
301
383
|
return if options[:in_dashboard]
|
|
302
384
|
when 'config'
|
|
303
|
-
handle_config_command(target, extra, options)
|
|
385
|
+
handle_config_command(target, extra, args[3], options)
|
|
304
386
|
exit 0 unless options[:in_dashboard]
|
|
305
387
|
return if options[:in_dashboard]
|
|
306
388
|
when 'connect', 'setup', 'init', 'install'
|
|
@@ -3985,8 +4067,56 @@ def self.handle_skills_command(subcommand, options)
|
|
|
3985
4067
|
puts
|
|
3986
4068
|
end
|
|
3987
4069
|
|
|
3988
|
-
def self.handle_config_command(subcommand, subvalue,
|
|
4070
|
+
def self.handle_config_command(subcommand, subvalue, subextra_or_options = nil, maybe_options = {})
|
|
4071
|
+
if subextra_or_options.is_a?(Hash)
|
|
4072
|
+
options = subextra_or_options
|
|
4073
|
+
subextra = nil
|
|
4074
|
+
else
|
|
4075
|
+
subextra = subextra_or_options
|
|
4076
|
+
options = maybe_options || {}
|
|
4077
|
+
end
|
|
3989
4078
|
case subcommand
|
|
4079
|
+
when 'set'
|
|
4080
|
+
key = subvalue.to_s.strip
|
|
4081
|
+
val = subextra.to_s.strip
|
|
4082
|
+
if key.empty? || val.empty?
|
|
4083
|
+
if options[:json]
|
|
4084
|
+
puts JSON.pretty_generate({ error: 'Usage: gsc config set <key> <value>' })
|
|
4085
|
+
else
|
|
4086
|
+
puts Color.c("❌ Error: Please specify key and value.", Color::RED)
|
|
4087
|
+
puts "Example: gsc config set opr_api_key opr_live_xxxx"
|
|
4088
|
+
puts "Example: gsc config set pagespeed_api_key AIzaSyxxxx"
|
|
4089
|
+
end
|
|
4090
|
+
exit 1
|
|
4091
|
+
end
|
|
4092
|
+
|
|
4093
|
+
Config.set(key, val)
|
|
4094
|
+
if key == 'opr_api_key'
|
|
4095
|
+
Config.set('openpagerank_api_key', val)
|
|
4096
|
+
elsif key == 'openpagerank_api_key'
|
|
4097
|
+
Config.set('opr_api_key', val)
|
|
4098
|
+
end
|
|
4099
|
+
|
|
4100
|
+
masked = val.length > 8 ? "#{val[0..7]}...#{val[-4..-1]}" : "***"
|
|
4101
|
+
if options[:json]
|
|
4102
|
+
puts JSON.pretty_generate({ status: 'ok', key: key, value: masked, configFile: Config::CONFIG_FILE })
|
|
4103
|
+
else
|
|
4104
|
+
puts BANNER
|
|
4105
|
+
puts Color.c("✅ Saved configuration: #{Color.c(key, Color::CYAN)} = #{Color.c(masked, Color::GREEN)}", Color::GREEN, Color::BOLD)
|
|
4106
|
+
puts Color.c(" 📁 Stored in #{Config::CONFIG_FILE}", Color::GRAY)
|
|
4107
|
+
end
|
|
4108
|
+
return
|
|
4109
|
+
|
|
4110
|
+
when 'get'
|
|
4111
|
+
key = subvalue.to_s.strip
|
|
4112
|
+
val = Config.get(key)
|
|
4113
|
+
if options[:json]
|
|
4114
|
+
puts JSON.pretty_generate({ key: key, value: val })
|
|
4115
|
+
else
|
|
4116
|
+
puts val || "(not set)"
|
|
4117
|
+
end
|
|
4118
|
+
return
|
|
4119
|
+
|
|
3990
4120
|
when 'set-domain', 'domain'
|
|
3991
4121
|
if subvalue.nil? || subvalue.empty?
|
|
3992
4122
|
if options[:json]
|
|
@@ -4092,7 +4222,28 @@ def self.handle_skills_command(subcommand, options)
|
|
|
4092
4222
|
else
|
|
4093
4223
|
puts " Key Location: #{Color.c('Not found', Color::RED)}"
|
|
4094
4224
|
end
|
|
4095
|
-
|
|
4225
|
+
opr = cfg['opr_api_key'] || cfg['openpagerank_api_key']
|
|
4226
|
+
if opr
|
|
4227
|
+
m_opr = opr.length > 8 ? "#{opr[0..7]}...#{opr[-4..-1]}" : "***"
|
|
4228
|
+
puts " OpenPageRank: #{Color.c(m_opr, Color::GREEN)} (Active)"
|
|
4229
|
+
else
|
|
4230
|
+
puts " OpenPageRank: #{Color.c('(not set - run: gsc config set opr_api_key <key>)', Color::GRAY)}"
|
|
4231
|
+
end
|
|
4232
|
+
|
|
4233
|
+
ps = cfg['pagespeed_api_key']
|
|
4234
|
+
if ps
|
|
4235
|
+
m_ps = ps.length > 8 ? "#{ps[0..7]}...#{ps[-4..-1]}" : "***"
|
|
4236
|
+
puts " PageSpeed API: #{Color.c(m_ps, Color::GREEN)} (Active)"
|
|
4237
|
+
end
|
|
4238
|
+
|
|
4239
|
+
ke = cfg['keywords_everywhere_api_key']
|
|
4240
|
+
if ke
|
|
4241
|
+
m_ke = ke.length > 8 ? "#{ke[0..7]}...#{ke[-4..-1]}" : "***"
|
|
4242
|
+
puts " Keywords Evr: #{Color.c(m_ke, Color::GREEN)} (Active)"
|
|
4243
|
+
end
|
|
4244
|
+
|
|
4245
|
+
puts "
|
|
4246
|
+
#{Color::BOLD}Linked GA4 Properties per Domain:#{Color::RESET}"
|
|
4096
4247
|
ga4_props = cfg['ga4_properties'] || {}
|
|
4097
4248
|
if ga4_props.empty?
|
|
4098
4249
|
puts " #{Color.c('(None linked yet. Use: gsc config set-ga4 <id> -d <domain>)', Color::GRAY)}"
|
|
@@ -4106,6 +4257,8 @@ def self.handle_skills_command(subcommand, options)
|
|
|
4106
4257
|
puts "#{Color::BOLD}Configuration shortcuts:#{Color::RESET}"
|
|
4107
4258
|
puts " gsc connect Interactive setup wizard (drag & drop key)"
|
|
4108
4259
|
puts " gsc use <domain> Set active default domain"
|
|
4260
|
+
puts " gsc config set <key> <val> Set API key (e.g. opr_api_key, pagespeed_api_key)"
|
|
4261
|
+
puts " gsc config get <key> Read a stored configuration value"
|
|
4109
4262
|
puts " gsc config set-ga4 <id> Link a GA4 Property ID to domain"
|
|
4110
4263
|
puts " gsc open Open config directory in Finder"
|
|
4111
4264
|
puts " gsc where Show install path and paths summary"
|