gsc-cli 2.2.3 → 2.2.5
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/README.md +7 -1
- data/bin/gsc +165 -17
- data/dist/gsc +165 -17
- data/lib/gsc/cli/audit.rb +12 -3
- data/lib/gsc/cli/indexing.rb +53 -4
- data/lib/gsc/cli.rb +12 -0
- data/lib/gsc/cli_advanced.rb +12 -3
- data/lib/gsc/command_registry.rb +1 -1
- data/lib/gsc/config.rb +7 -1
- data/lib/gsc/indexing_queue.rb +42 -3
- data/lib/gsc/page_speed.rb +24 -0
- data/lib/gsc/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b146c585e5fdbbea6d4b61644f052df6e75b418d64666d4b4b6006825f0e6eaf
|
|
4
|
+
data.tar.gz: 04b24385606b02d483f14db0759848890aba51714b2fca731b483d9d88bf3346
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3e2fa01bad3d160440d6ec93be6738dfeba193e7a89e380132b1f0f4c6abd8d2f311746ebe850c5af0cd95f303e3474996d0e640bdf336e2deec50482bedac4b
|
|
7
|
+
data.tar.gz: 9bbab9f7af4f23c285c7c22e5b8012a71633832a3c899b00ccf5e3f1ac1610b11bde8702cd6b5c6868641f0d55b359fdf72dba16d55cd877f662f5ba5134464a
|
data/README.md
CHANGED
|
@@ -494,13 +494,19 @@ gsc use 2
|
|
|
494
494
|
| `gsc index <url>` | — | Priority Googlebot crawl submission (`URL_UPDATED`) | `--dry-run`, `--json` |
|
|
495
495
|
| `gsc remove <url>` | — | Notify Googlebot of permanently deleted URL (`URL_DELETED`) | `--dry-run`, `--json` |
|
|
496
496
|
| `gsc status <url>` | — | Check Google Indexing API submission status and latest notification timestamp| `--json` |
|
|
497
|
-
| `gsc index-batch` | — | Batch URL indexing processor with daily 200-URL quota tracking | `--
|
|
497
|
+
| `gsc index-batch` | — | Batch URL indexing processor with daily 200-URL quota tracking | `--batch-size`, `--force`, `--dry-run`, `--json` |
|
|
498
498
|
| `gsc indexnow <url>` | — | Multi-engine instant submission (Bing, Yandex, Seznam, Naver) | `--key`, `--json` |
|
|
499
499
|
| `gsc zombies <sitemap>` | — | Detect zero-impression deadweight URLs wasting crawl budget over 90 days | `--days`, `--json` |
|
|
500
500
|
| `gsc sitemaps-list` | — | List registered XML sitemaps in Search Console | `--json` |
|
|
501
501
|
| `gsc sitemaps-submit <url>`| — | Submit or re-submit an XML sitemap to Search Console | `--json` |
|
|
502
502
|
| `gsc sitemap-tree <url>` | — | Visual sitemap hierarchy tree and URL limit validator | `--json` |
|
|
503
503
|
|
|
504
|
+
> 🛡️ **Batch Indexing Safety Guard (`gsc index-batch clear`)**:
|
|
505
|
+
> When automating bulk indexing in scripts, CI/CD, or AI agents, an accidental `clear` could wipe hundreds of queued URLs waiting to be indexed. To prevent queue loss:
|
|
506
|
+
> - **Interactive Shells**: Requires explicit `[y/N]` user confirmation.
|
|
507
|
+
> - **Non-Interactive / Scripts / Agents**: Requires `--force` / `-f` (aborts with exit code `1` if omitted).
|
|
508
|
+
> - **Automatic Backup**: Every clear automatically snapshots the pending queue to `~/.gsc/backups/queue_backup_<TIMESTAMP>.json` before wiping, so URLs are never lost.
|
|
509
|
+
|
|
504
510
|
### 6. Keyword Research & Demand Trends
|
|
505
511
|
| Command | Shortcut | Description | Flags |
|
|
506
512
|
|---|---|---|---|
|
data/bin/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.2.
|
|
6
|
+
# Standalone Single-File Distribution (Built from lib/gsc v2.2.5)
|
|
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.2.
|
|
23
|
+
VERSION = '2.2.5'
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
# --- color.rb ---
|
|
@@ -70,7 +70,13 @@ module GSC
|
|
|
70
70
|
CONFIG_DIR = File.expand_path('~/.config/gsc')
|
|
71
71
|
CONFIG_FILE = File.join(CONFIG_DIR, 'config.json')
|
|
72
72
|
|
|
73
|
-
def self.
|
|
73
|
+
def self.ensure_gsc_symlink!
|
|
74
|
+
gsc_dir = File.expand_path('~/.gsc')
|
|
75
|
+
return if File.exist?(gsc_dir) || File.symlink?(gsc_dir)
|
|
76
|
+
File.symlink(CONFIG_DIR, gsc_dir) rescue nil
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def self.get(key)
|
|
74
80
|
load[key.to_s]
|
|
75
81
|
end
|
|
76
82
|
|
|
@@ -3174,6 +3180,29 @@ module GSC
|
|
|
3174
3180
|
|
|
3175
3181
|
overall_category = data.dig('loadingExperience', 'overall_category') || data.dig('originLoadingExperience', 'overall_category')
|
|
3176
3182
|
|
|
3183
|
+
# Official Google Core Web Vitals Assessment evaluates ONLY the 3 Core Web Vitals:
|
|
3184
|
+
# LCP, INP (or legacy FID), and CLS. (FCP and TTFB are diagnostic metrics).
|
|
3185
|
+
# - PASSED: All present CWV metrics are 'FAST' (Good).
|
|
3186
|
+
# - POOR: Any CWV metric is 'SLOW' (Poor).
|
|
3187
|
+
# - NEEDS IMPROVEMENT: No CWV metric is 'SLOW', but at least one is 'AVERAGE'.
|
|
3188
|
+
cwv_keys = ['LARGEST_CONTENTFUL_PAINT_MS', 'INTERACTION_TO_NEXT_PAINT', 'CUMULATIVE_LAYOUT_SHIFT_SCORE']
|
|
3189
|
+
cwv_keys[1] = 'FIRST_INPUT_DELAY_MS' if !crux_metrics.key?('INTERACTION_TO_NEXT_PAINT') && crux_metrics.key?('FIRST_INPUT_DELAY_MS')
|
|
3190
|
+
|
|
3191
|
+
active_cwv = cwv_keys.map { |k| crux_metrics[k] }.compact
|
|
3192
|
+
cwv_assessment = if active_cwv.any?
|
|
3193
|
+
if active_cwv.any? { |m| m[:category] == 'SLOW' }
|
|
3194
|
+
'POOR'
|
|
3195
|
+
elsif active_cwv.any? { |m| m[:category] == 'AVERAGE' }
|
|
3196
|
+
'NEEDS IMPROVEMENT'
|
|
3197
|
+
elsif active_cwv.all? { |m| m[:category] == 'FAST' }
|
|
3198
|
+
'PASSED'
|
|
3199
|
+
else
|
|
3200
|
+
overall_category
|
|
3201
|
+
end
|
|
3202
|
+
else
|
|
3203
|
+
overall_category
|
|
3204
|
+
end
|
|
3205
|
+
|
|
3177
3206
|
# Opportunities
|
|
3178
3207
|
opportunities = []
|
|
3179
3208
|
audits.each do |k, v|
|
|
@@ -3205,6 +3234,7 @@ module GSC
|
|
|
3205
3234
|
field_data: crux_metrics,
|
|
3206
3235
|
field_source: crux_source,
|
|
3207
3236
|
overall_category: overall_category,
|
|
3237
|
+
cwv_assessment: cwv_assessment,
|
|
3208
3238
|
opportunities: opportunities.first(5)
|
|
3209
3239
|
}
|
|
3210
3240
|
end
|
|
@@ -5405,13 +5435,16 @@ require 'time'
|
|
|
5405
5435
|
module GSC
|
|
5406
5436
|
class IndexingQueue
|
|
5407
5437
|
QUEUE_FILE = File.join(Config::CONFIG_DIR, 'indexing_queue.json')
|
|
5438
|
+
BACKUPS_DIR = File.join(Config::CONFIG_DIR, 'backups')
|
|
5408
5439
|
DAILY_LIMIT = 200
|
|
5409
5440
|
|
|
5410
|
-
attr_reader :state
|
|
5441
|
+
attr_reader :state, :file_path, :backup_dir
|
|
5411
5442
|
|
|
5412
|
-
def initialize(file_path = QUEUE_FILE)
|
|
5443
|
+
def initialize(file_path = QUEUE_FILE, backup_dir: nil)
|
|
5413
5444
|
@file_path = file_path
|
|
5445
|
+
@backup_dir = backup_dir || File.join(File.dirname(@file_path), 'backups')
|
|
5414
5446
|
@state = load_state
|
|
5447
|
+
Config.ensure_gsc_symlink!
|
|
5415
5448
|
end
|
|
5416
5449
|
|
|
5417
5450
|
def add_urls(urls)
|
|
@@ -5443,7 +5476,41 @@ module GSC
|
|
|
5443
5476
|
}
|
|
5444
5477
|
end
|
|
5445
5478
|
|
|
5446
|
-
def
|
|
5479
|
+
def backup(scope = :all)
|
|
5480
|
+
pending = @state['pending'] || []
|
|
5481
|
+
submitted = @state['submitted'] || []
|
|
5482
|
+
failed = @state['failed'] || []
|
|
5483
|
+
|
|
5484
|
+
# Nothing to backup if completely empty
|
|
5485
|
+
return nil if pending.empty? && submitted.empty? && failed.empty?
|
|
5486
|
+
|
|
5487
|
+
FileUtils.mkdir_p(@backup_dir)
|
|
5488
|
+
timestamp = Time.now.utc.strftime('%Y%m%d_%H%M%S')
|
|
5489
|
+
backup_file = File.join(@backup_dir, "queue_backup_#{timestamp}.json")
|
|
5490
|
+
if File.exist?(backup_file)
|
|
5491
|
+
backup_file = File.join(@backup_dir, "queue_backup_#{timestamp}_#{Time.now.to_f.to_s.split('.').last}.json")
|
|
5492
|
+
end
|
|
5493
|
+
|
|
5494
|
+
snapshot = {
|
|
5495
|
+
'timestamp' => Time.now.utc.iso8601,
|
|
5496
|
+
'cleared_scope' => scope.to_s,
|
|
5497
|
+
'count' => pending.size,
|
|
5498
|
+
'pending' => pending,
|
|
5499
|
+
'submitted' => submitted,
|
|
5500
|
+
'failed' => failed,
|
|
5501
|
+
'daily_quota_used' => @state['daily_quota_used'] || 0,
|
|
5502
|
+
'last_reset_date' => @state['last_reset_date']
|
|
5503
|
+
}
|
|
5504
|
+
|
|
5505
|
+
File.write(backup_file, JSON.pretty_generate(snapshot))
|
|
5506
|
+
backup_file
|
|
5507
|
+
end
|
|
5508
|
+
|
|
5509
|
+
def clear(scope = :all, backup: true)
|
|
5510
|
+
pending = @state['pending'] || []
|
|
5511
|
+
count = pending.size
|
|
5512
|
+
backup_file = backup ? backup(scope) : nil
|
|
5513
|
+
|
|
5447
5514
|
if scope == :pending
|
|
5448
5515
|
@state['pending'] = []
|
|
5449
5516
|
else
|
|
@@ -5452,6 +5519,8 @@ module GSC
|
|
|
5452
5519
|
@state['failed'] = []
|
|
5453
5520
|
end
|
|
5454
5521
|
save_state
|
|
5522
|
+
|
|
5523
|
+
{ count: count, backup_file: backup_file }
|
|
5455
5524
|
end
|
|
5456
5525
|
|
|
5457
5526
|
def process_batch(api, batch_size: 50, dry_run: false, delay_sec: 0.15)
|
|
@@ -9039,7 +9108,7 @@ COMMAND_REGISTRY = [
|
|
|
9039
9108
|
commands: [
|
|
9040
9109
|
{ name: "inspect <url>", shortcut: nil, desc: "Live Google index check (coverage, canonical, date, robots.txt)", flags: ["-d"] },
|
|
9041
9110
|
{ name: "index <url>", shortcut: nil, desc: "Notify Googlebot to crawl/index URL immediately (URL_UPDATED)", flags: ["--dry-run"] },
|
|
9042
|
-
{ name: "index-batch [action]", shortcut: "ib", desc: "Automated 200 URL/day quota queue manager (add, run, status, clear)", flags: ["--batch-size", "--dry-run", "--json"] },
|
|
9111
|
+
{ name: "index-batch [action]", shortcut: "ib", desc: "Automated 200 URL/day quota queue manager (add, run, status, clear)", flags: ["--batch-size", "--dry-run", "--force", "--json"] },
|
|
9043
9112
|
{ name: "remove <url>", shortcut: nil, desc: "Notify Googlebot a page has been deleted (URL_DELETED)", flags: ["--dry-run"] },
|
|
9044
9113
|
{ name: "status <url>", shortcut: nil, desc: "Check Google Indexing API notification metadata", flags: [] },
|
|
9045
9114
|
{ name: "sitemaps-list", shortcut: nil, desc: "List registered XML sitemaps in Search Console", flags: ["-d"] },
|
|
@@ -21010,11 +21079,54 @@ module GSC
|
|
|
21010
21079
|
end
|
|
21011
21080
|
|
|
21012
21081
|
when 'clear', 'reset'
|
|
21013
|
-
queue.
|
|
21082
|
+
count = (queue.state['pending'] || []).size
|
|
21083
|
+
if count == 0
|
|
21084
|
+
if options[:json]
|
|
21085
|
+
puts JSON.pretty_generate({ ok: true, message: 'Batch queue is already empty.', count: 0 })
|
|
21086
|
+
else
|
|
21087
|
+
puts "Batch queue is already empty."
|
|
21088
|
+
end
|
|
21089
|
+
return
|
|
21090
|
+
end
|
|
21091
|
+
|
|
21092
|
+
unless options[:force]
|
|
21093
|
+
if $stdin.tty?
|
|
21094
|
+
$stdout.print "Are you sure you want to clear #{count} pending URLs from the batch queue? [y/N]: "
|
|
21095
|
+
$stdout.flush
|
|
21096
|
+
ans = $stdin.gets.to_s.strip.downcase
|
|
21097
|
+
unless ans == 'y' || ans == 'yes'
|
|
21098
|
+
if options[:json]
|
|
21099
|
+
puts JSON.pretty_generate({ ok: false, message: 'Clear aborted. Queue preserved.' })
|
|
21100
|
+
else
|
|
21101
|
+
puts "Clear aborted. Queue preserved."
|
|
21102
|
+
end
|
|
21103
|
+
return
|
|
21104
|
+
end
|
|
21105
|
+
else
|
|
21106
|
+
error_msg = "Error: 'gsc index-batch clear' requires '--force' in non-interactive environments to prevent accidental queue loss."
|
|
21107
|
+
if options[:json]
|
|
21108
|
+
puts JSON.pretty_generate({ error: error_msg })
|
|
21109
|
+
else
|
|
21110
|
+
puts error_msg
|
|
21111
|
+
end
|
|
21112
|
+
exit 1
|
|
21113
|
+
end
|
|
21114
|
+
end
|
|
21115
|
+
|
|
21116
|
+
res = queue.clear(:all, backup: true)
|
|
21117
|
+
backup_path = res[:backup_file]
|
|
21118
|
+
display_backup = backup_path ? backup_path.sub(/^#{Regexp.escape(Dir.home)}\/\.config\/gsc/, '~/.gsc').sub(/^#{Regexp.escape(Dir.home)}/, '~') : 'none'
|
|
21119
|
+
cleared_count = res[:count] || count
|
|
21120
|
+
|
|
21014
21121
|
if options[:json]
|
|
21015
|
-
puts JSON.pretty_generate({
|
|
21122
|
+
puts JSON.pretty_generate({
|
|
21123
|
+
ok: true,
|
|
21124
|
+
message: "Queue cleared (#{cleared_count} items). Backup saved to #{display_backup}",
|
|
21125
|
+
cleared_count: cleared_count,
|
|
21126
|
+
backup_file: backup_path
|
|
21127
|
+
})
|
|
21016
21128
|
else
|
|
21017
|
-
puts
|
|
21129
|
+
puts "Queue cleared (#{cleared_count} items). Backup saved to #{display_backup}"
|
|
21018
21130
|
end
|
|
21019
21131
|
|
|
21020
21132
|
else
|
|
@@ -21032,7 +21144,13 @@ module GSC
|
|
|
21032
21144
|
puts "\nCommands:"
|
|
21033
21145
|
puts " • #{Color.c('gsc index-batch add <url|sitemap.xml|file.txt>', Color::CYAN)} - Enqueue URLs"
|
|
21034
21146
|
puts " • #{Color.c('gsc index-batch run [--batch-size 50] [--dry-run]', Color::CYAN)} - Process queue"
|
|
21035
|
-
puts " • #{Color.c('gsc index-batch clear', Color::CYAN)}
|
|
21147
|
+
puts " • #{Color.c('gsc index-batch clear [--force]', Color::CYAN)} - Reset queue (safe wipe)"
|
|
21148
|
+
puts ""
|
|
21149
|
+
puts "🛡️ #{Color::BOLD}Queue Safety Guard:#{Color::RESET}"
|
|
21150
|
+
puts " • Prevents accidental queue loss from scripts, cron, or AI agents"
|
|
21151
|
+
puts " • Interactive: Prompts for confirmation [y/N] before clearing"
|
|
21152
|
+
puts " • Non-interactive / CI: Requires #{Color.c('--force', Color::YELLOW)} flag to proceed"
|
|
21153
|
+
puts " • Snapshot backup: Automatically saves a backup to ~/.gsc/backups/ before clearing"
|
|
21036
21154
|
puts ""
|
|
21037
21155
|
end
|
|
21038
21156
|
end
|
|
@@ -22857,10 +22975,19 @@ module GSC
|
|
|
22857
22975
|
fd = data[:field_data] || {}
|
|
22858
22976
|
if fd.any?
|
|
22859
22977
|
source_label = data[:field_source] == :origin ? "Domain-Wide Origin RUM" : "URL-Level RUM"
|
|
22860
|
-
|
|
22861
|
-
|
|
22978
|
+
assessment = data[:cwv_assessment] || data[:overall_category] || "UNKNOWN"
|
|
22979
|
+
assessment_badge = case assessment
|
|
22980
|
+
when 'PASSED', 'FAST'
|
|
22981
|
+
Color.c("PASSED (All 3 Metrics Good)", Color::GREEN, Color::BOLD)
|
|
22982
|
+
when 'NEEDS IMPROVEMENT', 'AVERAGE'
|
|
22983
|
+
Color.c("NEEDS IMPROVEMENT", Color::YELLOW, Color::BOLD)
|
|
22984
|
+
when 'POOR', 'SLOW', 'FAILED'
|
|
22985
|
+
Color.c("FAILED (Poor Metrics Detected)", Color::RED, Color::BOLD)
|
|
22986
|
+
else
|
|
22987
|
+
Color.c(assessment, Color::YELLOW, Color::BOLD)
|
|
22988
|
+
end
|
|
22862
22989
|
puts "\n#{Color::BOLD}🌐 CrUX FIELD DATA (28-Day Real User Monitoring - #{source_label}):#{Color::RESET}"
|
|
22863
|
-
puts " • Core Web Vitals
|
|
22990
|
+
puts " • Core Web Vitals Assessment : #{assessment_badge}"
|
|
22864
22991
|
|
|
22865
22992
|
if fd['LARGEST_CONTENTFUL_PAINT_MS']
|
|
22866
22993
|
val = "#{(fd['LARGEST_CONTENTFUL_PAINT_MS'][:percentile].to_f / 1000).round(2)} s"
|
|
@@ -30016,10 +30143,19 @@ module GSC
|
|
|
30016
30143
|
fd = data[:field_data] || {}
|
|
30017
30144
|
if fd.any?
|
|
30018
30145
|
source_label = data[:field_source] == :origin ? "Domain-Wide Origin RUM" : "URL-Level RUM"
|
|
30019
|
-
|
|
30020
|
-
|
|
30146
|
+
assessment = data[:cwv_assessment] || data[:overall_category] || "UNKNOWN"
|
|
30147
|
+
assessment_badge = case assessment
|
|
30148
|
+
when 'PASSED', 'FAST'
|
|
30149
|
+
Color.c("PASSED (All 3 Metrics Good)", Color::GREEN, Color::BOLD)
|
|
30150
|
+
when 'NEEDS IMPROVEMENT', 'AVERAGE'
|
|
30151
|
+
Color.c("NEEDS IMPROVEMENT", Color::YELLOW, Color::BOLD)
|
|
30152
|
+
when 'POOR', 'SLOW', 'FAILED'
|
|
30153
|
+
Color.c("FAILED (Poor Metrics Detected)", Color::RED, Color::BOLD)
|
|
30154
|
+
else
|
|
30155
|
+
Color.c(assessment, Color::YELLOW, Color::BOLD)
|
|
30156
|
+
end
|
|
30021
30157
|
puts "\n#{Color::BOLD}🌐 CrUX FIELD DATA (28-Day Real User Monitoring - #{source_label}):#{Color::RESET}"
|
|
30022
|
-
puts " • Core Web Vitals
|
|
30158
|
+
puts " • Core Web Vitals Assessment : #{assessment_badge}"
|
|
30023
30159
|
|
|
30024
30160
|
if fd['LARGEST_CONTENTFUL_PAINT_MS']
|
|
30025
30161
|
val = "#{(fd['LARGEST_CONTENTFUL_PAINT_MS'][:percentile].to_f / 1000).round(2)} s"
|
|
@@ -31529,6 +31665,10 @@ module GSC
|
|
|
31529
31665
|
options[:batch_size] = size
|
|
31530
31666
|
end
|
|
31531
31667
|
|
|
31668
|
+
opts.on('-f', '--force', '--yes', '-y', 'Bypass confirmation prompts (required for non-interactive queue clears)') do
|
|
31669
|
+
options[:force] = true
|
|
31670
|
+
end
|
|
31671
|
+
|
|
31532
31672
|
opts.on('--concurrency COUNT', Integer, 'Concurrent threads for multi-page site crawler (default: 5, max: 20)') do |c|
|
|
31533
31673
|
options[:concurrency] = c
|
|
31534
31674
|
end
|
|
@@ -32185,6 +32325,14 @@ module GSC
|
|
|
32185
32325
|
Audit.run(command, target, extra, options)
|
|
32186
32326
|
exit 0 unless options[:in_dashboard]
|
|
32187
32327
|
return
|
|
32328
|
+
|
|
32329
|
+
when 'index-batch', 'queue', 'batch-index'
|
|
32330
|
+
action = target.to_s.strip.downcase
|
|
32331
|
+
if action != 'run' && action != 'process' && action != 'execute'
|
|
32332
|
+
Indexing.run(command, target, extra, options, nil, nil, nil, nil)
|
|
32333
|
+
exit 0 unless options[:in_dashboard]
|
|
32334
|
+
return
|
|
32335
|
+
end
|
|
32188
32336
|
end
|
|
32189
32337
|
|
|
32190
32338
|
# 7. Authenticated Commands (GSC / Indexing / Analytics / GA4)
|
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.2.
|
|
6
|
+
# Standalone Single-File Distribution (Built from lib/gsc v2.2.5)
|
|
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.2.
|
|
23
|
+
VERSION = '2.2.5'
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
# --- color.rb ---
|
|
@@ -70,7 +70,13 @@ module GSC
|
|
|
70
70
|
CONFIG_DIR = File.expand_path('~/.config/gsc')
|
|
71
71
|
CONFIG_FILE = File.join(CONFIG_DIR, 'config.json')
|
|
72
72
|
|
|
73
|
-
def self.
|
|
73
|
+
def self.ensure_gsc_symlink!
|
|
74
|
+
gsc_dir = File.expand_path('~/.gsc')
|
|
75
|
+
return if File.exist?(gsc_dir) || File.symlink?(gsc_dir)
|
|
76
|
+
File.symlink(CONFIG_DIR, gsc_dir) rescue nil
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def self.get(key)
|
|
74
80
|
load[key.to_s]
|
|
75
81
|
end
|
|
76
82
|
|
|
@@ -3174,6 +3180,29 @@ module GSC
|
|
|
3174
3180
|
|
|
3175
3181
|
overall_category = data.dig('loadingExperience', 'overall_category') || data.dig('originLoadingExperience', 'overall_category')
|
|
3176
3182
|
|
|
3183
|
+
# Official Google Core Web Vitals Assessment evaluates ONLY the 3 Core Web Vitals:
|
|
3184
|
+
# LCP, INP (or legacy FID), and CLS. (FCP and TTFB are diagnostic metrics).
|
|
3185
|
+
# - PASSED: All present CWV metrics are 'FAST' (Good).
|
|
3186
|
+
# - POOR: Any CWV metric is 'SLOW' (Poor).
|
|
3187
|
+
# - NEEDS IMPROVEMENT: No CWV metric is 'SLOW', but at least one is 'AVERAGE'.
|
|
3188
|
+
cwv_keys = ['LARGEST_CONTENTFUL_PAINT_MS', 'INTERACTION_TO_NEXT_PAINT', 'CUMULATIVE_LAYOUT_SHIFT_SCORE']
|
|
3189
|
+
cwv_keys[1] = 'FIRST_INPUT_DELAY_MS' if !crux_metrics.key?('INTERACTION_TO_NEXT_PAINT') && crux_metrics.key?('FIRST_INPUT_DELAY_MS')
|
|
3190
|
+
|
|
3191
|
+
active_cwv = cwv_keys.map { |k| crux_metrics[k] }.compact
|
|
3192
|
+
cwv_assessment = if active_cwv.any?
|
|
3193
|
+
if active_cwv.any? { |m| m[:category] == 'SLOW' }
|
|
3194
|
+
'POOR'
|
|
3195
|
+
elsif active_cwv.any? { |m| m[:category] == 'AVERAGE' }
|
|
3196
|
+
'NEEDS IMPROVEMENT'
|
|
3197
|
+
elsif active_cwv.all? { |m| m[:category] == 'FAST' }
|
|
3198
|
+
'PASSED'
|
|
3199
|
+
else
|
|
3200
|
+
overall_category
|
|
3201
|
+
end
|
|
3202
|
+
else
|
|
3203
|
+
overall_category
|
|
3204
|
+
end
|
|
3205
|
+
|
|
3177
3206
|
# Opportunities
|
|
3178
3207
|
opportunities = []
|
|
3179
3208
|
audits.each do |k, v|
|
|
@@ -3205,6 +3234,7 @@ module GSC
|
|
|
3205
3234
|
field_data: crux_metrics,
|
|
3206
3235
|
field_source: crux_source,
|
|
3207
3236
|
overall_category: overall_category,
|
|
3237
|
+
cwv_assessment: cwv_assessment,
|
|
3208
3238
|
opportunities: opportunities.first(5)
|
|
3209
3239
|
}
|
|
3210
3240
|
end
|
|
@@ -5405,13 +5435,16 @@ require 'time'
|
|
|
5405
5435
|
module GSC
|
|
5406
5436
|
class IndexingQueue
|
|
5407
5437
|
QUEUE_FILE = File.join(Config::CONFIG_DIR, 'indexing_queue.json')
|
|
5438
|
+
BACKUPS_DIR = File.join(Config::CONFIG_DIR, 'backups')
|
|
5408
5439
|
DAILY_LIMIT = 200
|
|
5409
5440
|
|
|
5410
|
-
attr_reader :state
|
|
5441
|
+
attr_reader :state, :file_path, :backup_dir
|
|
5411
5442
|
|
|
5412
|
-
def initialize(file_path = QUEUE_FILE)
|
|
5443
|
+
def initialize(file_path = QUEUE_FILE, backup_dir: nil)
|
|
5413
5444
|
@file_path = file_path
|
|
5445
|
+
@backup_dir = backup_dir || File.join(File.dirname(@file_path), 'backups')
|
|
5414
5446
|
@state = load_state
|
|
5447
|
+
Config.ensure_gsc_symlink!
|
|
5415
5448
|
end
|
|
5416
5449
|
|
|
5417
5450
|
def add_urls(urls)
|
|
@@ -5443,7 +5476,41 @@ module GSC
|
|
|
5443
5476
|
}
|
|
5444
5477
|
end
|
|
5445
5478
|
|
|
5446
|
-
def
|
|
5479
|
+
def backup(scope = :all)
|
|
5480
|
+
pending = @state['pending'] || []
|
|
5481
|
+
submitted = @state['submitted'] || []
|
|
5482
|
+
failed = @state['failed'] || []
|
|
5483
|
+
|
|
5484
|
+
# Nothing to backup if completely empty
|
|
5485
|
+
return nil if pending.empty? && submitted.empty? && failed.empty?
|
|
5486
|
+
|
|
5487
|
+
FileUtils.mkdir_p(@backup_dir)
|
|
5488
|
+
timestamp = Time.now.utc.strftime('%Y%m%d_%H%M%S')
|
|
5489
|
+
backup_file = File.join(@backup_dir, "queue_backup_#{timestamp}.json")
|
|
5490
|
+
if File.exist?(backup_file)
|
|
5491
|
+
backup_file = File.join(@backup_dir, "queue_backup_#{timestamp}_#{Time.now.to_f.to_s.split('.').last}.json")
|
|
5492
|
+
end
|
|
5493
|
+
|
|
5494
|
+
snapshot = {
|
|
5495
|
+
'timestamp' => Time.now.utc.iso8601,
|
|
5496
|
+
'cleared_scope' => scope.to_s,
|
|
5497
|
+
'count' => pending.size,
|
|
5498
|
+
'pending' => pending,
|
|
5499
|
+
'submitted' => submitted,
|
|
5500
|
+
'failed' => failed,
|
|
5501
|
+
'daily_quota_used' => @state['daily_quota_used'] || 0,
|
|
5502
|
+
'last_reset_date' => @state['last_reset_date']
|
|
5503
|
+
}
|
|
5504
|
+
|
|
5505
|
+
File.write(backup_file, JSON.pretty_generate(snapshot))
|
|
5506
|
+
backup_file
|
|
5507
|
+
end
|
|
5508
|
+
|
|
5509
|
+
def clear(scope = :all, backup: true)
|
|
5510
|
+
pending = @state['pending'] || []
|
|
5511
|
+
count = pending.size
|
|
5512
|
+
backup_file = backup ? backup(scope) : nil
|
|
5513
|
+
|
|
5447
5514
|
if scope == :pending
|
|
5448
5515
|
@state['pending'] = []
|
|
5449
5516
|
else
|
|
@@ -5452,6 +5519,8 @@ module GSC
|
|
|
5452
5519
|
@state['failed'] = []
|
|
5453
5520
|
end
|
|
5454
5521
|
save_state
|
|
5522
|
+
|
|
5523
|
+
{ count: count, backup_file: backup_file }
|
|
5455
5524
|
end
|
|
5456
5525
|
|
|
5457
5526
|
def process_batch(api, batch_size: 50, dry_run: false, delay_sec: 0.15)
|
|
@@ -9039,7 +9108,7 @@ COMMAND_REGISTRY = [
|
|
|
9039
9108
|
commands: [
|
|
9040
9109
|
{ name: "inspect <url>", shortcut: nil, desc: "Live Google index check (coverage, canonical, date, robots.txt)", flags: ["-d"] },
|
|
9041
9110
|
{ name: "index <url>", shortcut: nil, desc: "Notify Googlebot to crawl/index URL immediately (URL_UPDATED)", flags: ["--dry-run"] },
|
|
9042
|
-
{ name: "index-batch [action]", shortcut: "ib", desc: "Automated 200 URL/day quota queue manager (add, run, status, clear)", flags: ["--batch-size", "--dry-run", "--json"] },
|
|
9111
|
+
{ name: "index-batch [action]", shortcut: "ib", desc: "Automated 200 URL/day quota queue manager (add, run, status, clear)", flags: ["--batch-size", "--dry-run", "--force", "--json"] },
|
|
9043
9112
|
{ name: "remove <url>", shortcut: nil, desc: "Notify Googlebot a page has been deleted (URL_DELETED)", flags: ["--dry-run"] },
|
|
9044
9113
|
{ name: "status <url>", shortcut: nil, desc: "Check Google Indexing API notification metadata", flags: [] },
|
|
9045
9114
|
{ name: "sitemaps-list", shortcut: nil, desc: "List registered XML sitemaps in Search Console", flags: ["-d"] },
|
|
@@ -21010,11 +21079,54 @@ module GSC
|
|
|
21010
21079
|
end
|
|
21011
21080
|
|
|
21012
21081
|
when 'clear', 'reset'
|
|
21013
|
-
queue.
|
|
21082
|
+
count = (queue.state['pending'] || []).size
|
|
21083
|
+
if count == 0
|
|
21084
|
+
if options[:json]
|
|
21085
|
+
puts JSON.pretty_generate({ ok: true, message: 'Batch queue is already empty.', count: 0 })
|
|
21086
|
+
else
|
|
21087
|
+
puts "Batch queue is already empty."
|
|
21088
|
+
end
|
|
21089
|
+
return
|
|
21090
|
+
end
|
|
21091
|
+
|
|
21092
|
+
unless options[:force]
|
|
21093
|
+
if $stdin.tty?
|
|
21094
|
+
$stdout.print "Are you sure you want to clear #{count} pending URLs from the batch queue? [y/N]: "
|
|
21095
|
+
$stdout.flush
|
|
21096
|
+
ans = $stdin.gets.to_s.strip.downcase
|
|
21097
|
+
unless ans == 'y' || ans == 'yes'
|
|
21098
|
+
if options[:json]
|
|
21099
|
+
puts JSON.pretty_generate({ ok: false, message: 'Clear aborted. Queue preserved.' })
|
|
21100
|
+
else
|
|
21101
|
+
puts "Clear aborted. Queue preserved."
|
|
21102
|
+
end
|
|
21103
|
+
return
|
|
21104
|
+
end
|
|
21105
|
+
else
|
|
21106
|
+
error_msg = "Error: 'gsc index-batch clear' requires '--force' in non-interactive environments to prevent accidental queue loss."
|
|
21107
|
+
if options[:json]
|
|
21108
|
+
puts JSON.pretty_generate({ error: error_msg })
|
|
21109
|
+
else
|
|
21110
|
+
puts error_msg
|
|
21111
|
+
end
|
|
21112
|
+
exit 1
|
|
21113
|
+
end
|
|
21114
|
+
end
|
|
21115
|
+
|
|
21116
|
+
res = queue.clear(:all, backup: true)
|
|
21117
|
+
backup_path = res[:backup_file]
|
|
21118
|
+
display_backup = backup_path ? backup_path.sub(/^#{Regexp.escape(Dir.home)}\/\.config\/gsc/, '~/.gsc').sub(/^#{Regexp.escape(Dir.home)}/, '~') : 'none'
|
|
21119
|
+
cleared_count = res[:count] || count
|
|
21120
|
+
|
|
21014
21121
|
if options[:json]
|
|
21015
|
-
puts JSON.pretty_generate({
|
|
21122
|
+
puts JSON.pretty_generate({
|
|
21123
|
+
ok: true,
|
|
21124
|
+
message: "Queue cleared (#{cleared_count} items). Backup saved to #{display_backup}",
|
|
21125
|
+
cleared_count: cleared_count,
|
|
21126
|
+
backup_file: backup_path
|
|
21127
|
+
})
|
|
21016
21128
|
else
|
|
21017
|
-
puts
|
|
21129
|
+
puts "Queue cleared (#{cleared_count} items). Backup saved to #{display_backup}"
|
|
21018
21130
|
end
|
|
21019
21131
|
|
|
21020
21132
|
else
|
|
@@ -21032,7 +21144,13 @@ module GSC
|
|
|
21032
21144
|
puts "\nCommands:"
|
|
21033
21145
|
puts " • #{Color.c('gsc index-batch add <url|sitemap.xml|file.txt>', Color::CYAN)} - Enqueue URLs"
|
|
21034
21146
|
puts " • #{Color.c('gsc index-batch run [--batch-size 50] [--dry-run]', Color::CYAN)} - Process queue"
|
|
21035
|
-
puts " • #{Color.c('gsc index-batch clear', Color::CYAN)}
|
|
21147
|
+
puts " • #{Color.c('gsc index-batch clear [--force]', Color::CYAN)} - Reset queue (safe wipe)"
|
|
21148
|
+
puts ""
|
|
21149
|
+
puts "🛡️ #{Color::BOLD}Queue Safety Guard:#{Color::RESET}"
|
|
21150
|
+
puts " • Prevents accidental queue loss from scripts, cron, or AI agents"
|
|
21151
|
+
puts " • Interactive: Prompts for confirmation [y/N] before clearing"
|
|
21152
|
+
puts " • Non-interactive / CI: Requires #{Color.c('--force', Color::YELLOW)} flag to proceed"
|
|
21153
|
+
puts " • Snapshot backup: Automatically saves a backup to ~/.gsc/backups/ before clearing"
|
|
21036
21154
|
puts ""
|
|
21037
21155
|
end
|
|
21038
21156
|
end
|
|
@@ -22857,10 +22975,19 @@ module GSC
|
|
|
22857
22975
|
fd = data[:field_data] || {}
|
|
22858
22976
|
if fd.any?
|
|
22859
22977
|
source_label = data[:field_source] == :origin ? "Domain-Wide Origin RUM" : "URL-Level RUM"
|
|
22860
|
-
|
|
22861
|
-
|
|
22978
|
+
assessment = data[:cwv_assessment] || data[:overall_category] || "UNKNOWN"
|
|
22979
|
+
assessment_badge = case assessment
|
|
22980
|
+
when 'PASSED', 'FAST'
|
|
22981
|
+
Color.c("PASSED (All 3 Metrics Good)", Color::GREEN, Color::BOLD)
|
|
22982
|
+
when 'NEEDS IMPROVEMENT', 'AVERAGE'
|
|
22983
|
+
Color.c("NEEDS IMPROVEMENT", Color::YELLOW, Color::BOLD)
|
|
22984
|
+
when 'POOR', 'SLOW', 'FAILED'
|
|
22985
|
+
Color.c("FAILED (Poor Metrics Detected)", Color::RED, Color::BOLD)
|
|
22986
|
+
else
|
|
22987
|
+
Color.c(assessment, Color::YELLOW, Color::BOLD)
|
|
22988
|
+
end
|
|
22862
22989
|
puts "\n#{Color::BOLD}🌐 CrUX FIELD DATA (28-Day Real User Monitoring - #{source_label}):#{Color::RESET}"
|
|
22863
|
-
puts " • Core Web Vitals
|
|
22990
|
+
puts " • Core Web Vitals Assessment : #{assessment_badge}"
|
|
22864
22991
|
|
|
22865
22992
|
if fd['LARGEST_CONTENTFUL_PAINT_MS']
|
|
22866
22993
|
val = "#{(fd['LARGEST_CONTENTFUL_PAINT_MS'][:percentile].to_f / 1000).round(2)} s"
|
|
@@ -30016,10 +30143,19 @@ module GSC
|
|
|
30016
30143
|
fd = data[:field_data] || {}
|
|
30017
30144
|
if fd.any?
|
|
30018
30145
|
source_label = data[:field_source] == :origin ? "Domain-Wide Origin RUM" : "URL-Level RUM"
|
|
30019
|
-
|
|
30020
|
-
|
|
30146
|
+
assessment = data[:cwv_assessment] || data[:overall_category] || "UNKNOWN"
|
|
30147
|
+
assessment_badge = case assessment
|
|
30148
|
+
when 'PASSED', 'FAST'
|
|
30149
|
+
Color.c("PASSED (All 3 Metrics Good)", Color::GREEN, Color::BOLD)
|
|
30150
|
+
when 'NEEDS IMPROVEMENT', 'AVERAGE'
|
|
30151
|
+
Color.c("NEEDS IMPROVEMENT", Color::YELLOW, Color::BOLD)
|
|
30152
|
+
when 'POOR', 'SLOW', 'FAILED'
|
|
30153
|
+
Color.c("FAILED (Poor Metrics Detected)", Color::RED, Color::BOLD)
|
|
30154
|
+
else
|
|
30155
|
+
Color.c(assessment, Color::YELLOW, Color::BOLD)
|
|
30156
|
+
end
|
|
30021
30157
|
puts "\n#{Color::BOLD}🌐 CrUX FIELD DATA (28-Day Real User Monitoring - #{source_label}):#{Color::RESET}"
|
|
30022
|
-
puts " • Core Web Vitals
|
|
30158
|
+
puts " • Core Web Vitals Assessment : #{assessment_badge}"
|
|
30023
30159
|
|
|
30024
30160
|
if fd['LARGEST_CONTENTFUL_PAINT_MS']
|
|
30025
30161
|
val = "#{(fd['LARGEST_CONTENTFUL_PAINT_MS'][:percentile].to_f / 1000).round(2)} s"
|
|
@@ -31529,6 +31665,10 @@ module GSC
|
|
|
31529
31665
|
options[:batch_size] = size
|
|
31530
31666
|
end
|
|
31531
31667
|
|
|
31668
|
+
opts.on('-f', '--force', '--yes', '-y', 'Bypass confirmation prompts (required for non-interactive queue clears)') do
|
|
31669
|
+
options[:force] = true
|
|
31670
|
+
end
|
|
31671
|
+
|
|
31532
31672
|
opts.on('--concurrency COUNT', Integer, 'Concurrent threads for multi-page site crawler (default: 5, max: 20)') do |c|
|
|
31533
31673
|
options[:concurrency] = c
|
|
31534
31674
|
end
|
|
@@ -32185,6 +32325,14 @@ module GSC
|
|
|
32185
32325
|
Audit.run(command, target, extra, options)
|
|
32186
32326
|
exit 0 unless options[:in_dashboard]
|
|
32187
32327
|
return
|
|
32328
|
+
|
|
32329
|
+
when 'index-batch', 'queue', 'batch-index'
|
|
32330
|
+
action = target.to_s.strip.downcase
|
|
32331
|
+
if action != 'run' && action != 'process' && action != 'execute'
|
|
32332
|
+
Indexing.run(command, target, extra, options, nil, nil, nil, nil)
|
|
32333
|
+
exit 0 unless options[:in_dashboard]
|
|
32334
|
+
return
|
|
32335
|
+
end
|
|
32188
32336
|
end
|
|
32189
32337
|
|
|
32190
32338
|
# 7. Authenticated Commands (GSC / Indexing / Analytics / GA4)
|
data/lib/gsc/cli/audit.rb
CHANGED
|
@@ -170,10 +170,19 @@ module GSC
|
|
|
170
170
|
fd = data[:field_data] || {}
|
|
171
171
|
if fd.any?
|
|
172
172
|
source_label = data[:field_source] == :origin ? "Domain-Wide Origin RUM" : "URL-Level RUM"
|
|
173
|
-
|
|
174
|
-
|
|
173
|
+
assessment = data[:cwv_assessment] || data[:overall_category] || "UNKNOWN"
|
|
174
|
+
assessment_badge = case assessment
|
|
175
|
+
when 'PASSED', 'FAST'
|
|
176
|
+
Color.c("PASSED (All 3 Metrics Good)", Color::GREEN, Color::BOLD)
|
|
177
|
+
when 'NEEDS IMPROVEMENT', 'AVERAGE'
|
|
178
|
+
Color.c("NEEDS IMPROVEMENT", Color::YELLOW, Color::BOLD)
|
|
179
|
+
when 'POOR', 'SLOW', 'FAILED'
|
|
180
|
+
Color.c("FAILED (Poor Metrics Detected)", Color::RED, Color::BOLD)
|
|
181
|
+
else
|
|
182
|
+
Color.c(assessment, Color::YELLOW, Color::BOLD)
|
|
183
|
+
end
|
|
175
184
|
puts "\n#{Color::BOLD}🌐 CrUX FIELD DATA (28-Day Real User Monitoring - #{source_label}):#{Color::RESET}"
|
|
176
|
-
puts " • Core Web Vitals
|
|
185
|
+
puts " • Core Web Vitals Assessment : #{assessment_badge}"
|
|
177
186
|
|
|
178
187
|
if fd['LARGEST_CONTENTFUL_PAINT_MS']
|
|
179
188
|
val = "#{(fd['LARGEST_CONTENTFUL_PAINT_MS'][:percentile].to_f / 1000).round(2)} s"
|
data/lib/gsc/cli/indexing.rb
CHANGED
|
@@ -226,11 +226,54 @@ module GSC
|
|
|
226
226
|
end
|
|
227
227
|
|
|
228
228
|
when 'clear', 'reset'
|
|
229
|
-
queue.
|
|
229
|
+
count = (queue.state['pending'] || []).size
|
|
230
|
+
if count == 0
|
|
231
|
+
if options[:json]
|
|
232
|
+
puts JSON.pretty_generate({ ok: true, message: 'Batch queue is already empty.', count: 0 })
|
|
233
|
+
else
|
|
234
|
+
puts "Batch queue is already empty."
|
|
235
|
+
end
|
|
236
|
+
return
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
unless options[:force]
|
|
240
|
+
if $stdin.tty?
|
|
241
|
+
$stdout.print "Are you sure you want to clear #{count} pending URLs from the batch queue? [y/N]: "
|
|
242
|
+
$stdout.flush
|
|
243
|
+
ans = $stdin.gets.to_s.strip.downcase
|
|
244
|
+
unless ans == 'y' || ans == 'yes'
|
|
245
|
+
if options[:json]
|
|
246
|
+
puts JSON.pretty_generate({ ok: false, message: 'Clear aborted. Queue preserved.' })
|
|
247
|
+
else
|
|
248
|
+
puts "Clear aborted. Queue preserved."
|
|
249
|
+
end
|
|
250
|
+
return
|
|
251
|
+
end
|
|
252
|
+
else
|
|
253
|
+
error_msg = "Error: 'gsc index-batch clear' requires '--force' in non-interactive environments to prevent accidental queue loss."
|
|
254
|
+
if options[:json]
|
|
255
|
+
puts JSON.pretty_generate({ error: error_msg })
|
|
256
|
+
else
|
|
257
|
+
puts error_msg
|
|
258
|
+
end
|
|
259
|
+
exit 1
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
res = queue.clear(:all, backup: true)
|
|
264
|
+
backup_path = res[:backup_file]
|
|
265
|
+
display_backup = backup_path ? backup_path.sub(/^#{Regexp.escape(Dir.home)}\/\.config\/gsc/, '~/.gsc').sub(/^#{Regexp.escape(Dir.home)}/, '~') : 'none'
|
|
266
|
+
cleared_count = res[:count] || count
|
|
267
|
+
|
|
230
268
|
if options[:json]
|
|
231
|
-
puts JSON.pretty_generate({
|
|
269
|
+
puts JSON.pretty_generate({
|
|
270
|
+
ok: true,
|
|
271
|
+
message: "Queue cleared (#{cleared_count} items). Backup saved to #{display_backup}",
|
|
272
|
+
cleared_count: cleared_count,
|
|
273
|
+
backup_file: backup_path
|
|
274
|
+
})
|
|
232
275
|
else
|
|
233
|
-
puts
|
|
276
|
+
puts "Queue cleared (#{cleared_count} items). Backup saved to #{display_backup}"
|
|
234
277
|
end
|
|
235
278
|
|
|
236
279
|
else
|
|
@@ -248,7 +291,13 @@ module GSC
|
|
|
248
291
|
puts "\nCommands:"
|
|
249
292
|
puts " • #{Color.c('gsc index-batch add <url|sitemap.xml|file.txt>', Color::CYAN)} - Enqueue URLs"
|
|
250
293
|
puts " • #{Color.c('gsc index-batch run [--batch-size 50] [--dry-run]', Color::CYAN)} - Process queue"
|
|
251
|
-
puts " • #{Color.c('gsc index-batch clear', Color::CYAN)}
|
|
294
|
+
puts " • #{Color.c('gsc index-batch clear [--force]', Color::CYAN)} - Reset queue (safe wipe)"
|
|
295
|
+
puts ""
|
|
296
|
+
puts "🛡️ #{Color::BOLD}Queue Safety Guard:#{Color::RESET}"
|
|
297
|
+
puts " • Prevents accidental queue loss from scripts, cron, or AI agents"
|
|
298
|
+
puts " • Interactive: Prompts for confirmation [y/N] before clearing"
|
|
299
|
+
puts " • Non-interactive / CI: Requires #{Color.c('--force', Color::YELLOW)} flag to proceed"
|
|
300
|
+
puts " • Snapshot backup: Automatically saves a backup to ~/.gsc/backups/ before clearing"
|
|
252
301
|
puts ""
|
|
253
302
|
end
|
|
254
303
|
end
|
data/lib/gsc/cli.rb
CHANGED
|
@@ -134,6 +134,10 @@ module GSC
|
|
|
134
134
|
options[:batch_size] = size
|
|
135
135
|
end
|
|
136
136
|
|
|
137
|
+
opts.on('-f', '--force', '--yes', '-y', 'Bypass confirmation prompts (required for non-interactive queue clears)') do
|
|
138
|
+
options[:force] = true
|
|
139
|
+
end
|
|
140
|
+
|
|
137
141
|
opts.on('--concurrency COUNT', Integer, 'Concurrent threads for multi-page site crawler (default: 5, max: 20)') do |c|
|
|
138
142
|
options[:concurrency] = c
|
|
139
143
|
end
|
|
@@ -790,6 +794,14 @@ module GSC
|
|
|
790
794
|
Audit.run(command, target, extra, options)
|
|
791
795
|
exit 0 unless options[:in_dashboard]
|
|
792
796
|
return
|
|
797
|
+
|
|
798
|
+
when 'index-batch', 'queue', 'batch-index'
|
|
799
|
+
action = target.to_s.strip.downcase
|
|
800
|
+
if action != 'run' && action != 'process' && action != 'execute'
|
|
801
|
+
Indexing.run(command, target, extra, options, nil, nil, nil, nil)
|
|
802
|
+
exit 0 unless options[:in_dashboard]
|
|
803
|
+
return
|
|
804
|
+
end
|
|
793
805
|
end
|
|
794
806
|
|
|
795
807
|
# 7. Authenticated Commands (GSC / Indexing / Analytics / GA4)
|
data/lib/gsc/cli_advanced.rb
CHANGED
|
@@ -180,10 +180,19 @@ module GSC
|
|
|
180
180
|
fd = data[:field_data] || {}
|
|
181
181
|
if fd.any?
|
|
182
182
|
source_label = data[:field_source] == :origin ? "Domain-Wide Origin RUM" : "URL-Level RUM"
|
|
183
|
-
|
|
184
|
-
|
|
183
|
+
assessment = data[:cwv_assessment] || data[:overall_category] || "UNKNOWN"
|
|
184
|
+
assessment_badge = case assessment
|
|
185
|
+
when 'PASSED', 'FAST'
|
|
186
|
+
Color.c("PASSED (All 3 Metrics Good)", Color::GREEN, Color::BOLD)
|
|
187
|
+
when 'NEEDS IMPROVEMENT', 'AVERAGE'
|
|
188
|
+
Color.c("NEEDS IMPROVEMENT", Color::YELLOW, Color::BOLD)
|
|
189
|
+
when 'POOR', 'SLOW', 'FAILED'
|
|
190
|
+
Color.c("FAILED (Poor Metrics Detected)", Color::RED, Color::BOLD)
|
|
191
|
+
else
|
|
192
|
+
Color.c(assessment, Color::YELLOW, Color::BOLD)
|
|
193
|
+
end
|
|
185
194
|
puts "\n#{Color::BOLD}🌐 CrUX FIELD DATA (28-Day Real User Monitoring - #{source_label}):#{Color::RESET}"
|
|
186
|
-
puts " • Core Web Vitals
|
|
195
|
+
puts " • Core Web Vitals Assessment : #{assessment_badge}"
|
|
187
196
|
|
|
188
197
|
if fd['LARGEST_CONTENTFUL_PAINT_MS']
|
|
189
198
|
val = "#{(fd['LARGEST_CONTENTFUL_PAINT_MS'][:percentile].to_f / 1000).round(2)} s"
|
data/lib/gsc/command_registry.rb
CHANGED
|
@@ -58,7 +58,7 @@ COMMAND_REGISTRY = [
|
|
|
58
58
|
commands: [
|
|
59
59
|
{ name: "inspect <url>", shortcut: nil, desc: "Live Google index check (coverage, canonical, date, robots.txt)", flags: ["-d"] },
|
|
60
60
|
{ name: "index <url>", shortcut: nil, desc: "Notify Googlebot to crawl/index URL immediately (URL_UPDATED)", flags: ["--dry-run"] },
|
|
61
|
-
{ name: "index-batch [action]", shortcut: "ib", desc: "Automated 200 URL/day quota queue manager (add, run, status, clear)", flags: ["--batch-size", "--dry-run", "--json"] },
|
|
61
|
+
{ name: "index-batch [action]", shortcut: "ib", desc: "Automated 200 URL/day quota queue manager (add, run, status, clear)", flags: ["--batch-size", "--dry-run", "--force", "--json"] },
|
|
62
62
|
{ name: "remove <url>", shortcut: nil, desc: "Notify Googlebot a page has been deleted (URL_DELETED)", flags: ["--dry-run"] },
|
|
63
63
|
{ name: "status <url>", shortcut: nil, desc: "Check Google Indexing API notification metadata", flags: [] },
|
|
64
64
|
{ name: "sitemaps-list", shortcut: nil, desc: "List registered XML sitemaps in Search Console", flags: ["-d"] },
|
data/lib/gsc/config.rb
CHANGED
|
@@ -9,7 +9,13 @@ module GSC
|
|
|
9
9
|
CONFIG_DIR = File.expand_path('~/.config/gsc')
|
|
10
10
|
CONFIG_FILE = File.join(CONFIG_DIR, 'config.json')
|
|
11
11
|
|
|
12
|
-
def self.
|
|
12
|
+
def self.ensure_gsc_symlink!
|
|
13
|
+
gsc_dir = File.expand_path('~/.gsc')
|
|
14
|
+
return if File.exist?(gsc_dir) || File.symlink?(gsc_dir)
|
|
15
|
+
File.symlink(CONFIG_DIR, gsc_dir) rescue nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.get(key)
|
|
13
19
|
load[key.to_s]
|
|
14
20
|
end
|
|
15
21
|
|
data/lib/gsc/indexing_queue.rb
CHANGED
|
@@ -8,13 +8,16 @@ require 'time'
|
|
|
8
8
|
module GSC
|
|
9
9
|
class IndexingQueue
|
|
10
10
|
QUEUE_FILE = File.join(Config::CONFIG_DIR, 'indexing_queue.json')
|
|
11
|
+
BACKUPS_DIR = File.join(Config::CONFIG_DIR, 'backups')
|
|
11
12
|
DAILY_LIMIT = 200
|
|
12
13
|
|
|
13
|
-
attr_reader :state
|
|
14
|
+
attr_reader :state, :file_path, :backup_dir
|
|
14
15
|
|
|
15
|
-
def initialize(file_path = QUEUE_FILE)
|
|
16
|
+
def initialize(file_path = QUEUE_FILE, backup_dir: nil)
|
|
16
17
|
@file_path = file_path
|
|
18
|
+
@backup_dir = backup_dir || File.join(File.dirname(@file_path), 'backups')
|
|
17
19
|
@state = load_state
|
|
20
|
+
Config.ensure_gsc_symlink!
|
|
18
21
|
end
|
|
19
22
|
|
|
20
23
|
def add_urls(urls)
|
|
@@ -46,7 +49,41 @@ module GSC
|
|
|
46
49
|
}
|
|
47
50
|
end
|
|
48
51
|
|
|
49
|
-
def
|
|
52
|
+
def backup(scope = :all)
|
|
53
|
+
pending = @state['pending'] || []
|
|
54
|
+
submitted = @state['submitted'] || []
|
|
55
|
+
failed = @state['failed'] || []
|
|
56
|
+
|
|
57
|
+
# Nothing to backup if completely empty
|
|
58
|
+
return nil if pending.empty? && submitted.empty? && failed.empty?
|
|
59
|
+
|
|
60
|
+
FileUtils.mkdir_p(@backup_dir)
|
|
61
|
+
timestamp = Time.now.utc.strftime('%Y%m%d_%H%M%S')
|
|
62
|
+
backup_file = File.join(@backup_dir, "queue_backup_#{timestamp}.json")
|
|
63
|
+
if File.exist?(backup_file)
|
|
64
|
+
backup_file = File.join(@backup_dir, "queue_backup_#{timestamp}_#{Time.now.to_f.to_s.split('.').last}.json")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
snapshot = {
|
|
68
|
+
'timestamp' => Time.now.utc.iso8601,
|
|
69
|
+
'cleared_scope' => scope.to_s,
|
|
70
|
+
'count' => pending.size,
|
|
71
|
+
'pending' => pending,
|
|
72
|
+
'submitted' => submitted,
|
|
73
|
+
'failed' => failed,
|
|
74
|
+
'daily_quota_used' => @state['daily_quota_used'] || 0,
|
|
75
|
+
'last_reset_date' => @state['last_reset_date']
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
File.write(backup_file, JSON.pretty_generate(snapshot))
|
|
79
|
+
backup_file
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def clear(scope = :all, backup: true)
|
|
83
|
+
pending = @state['pending'] || []
|
|
84
|
+
count = pending.size
|
|
85
|
+
backup_file = backup ? backup(scope) : nil
|
|
86
|
+
|
|
50
87
|
if scope == :pending
|
|
51
88
|
@state['pending'] = []
|
|
52
89
|
else
|
|
@@ -55,6 +92,8 @@ module GSC
|
|
|
55
92
|
@state['failed'] = []
|
|
56
93
|
end
|
|
57
94
|
save_state
|
|
95
|
+
|
|
96
|
+
{ count: count, backup_file: backup_file }
|
|
58
97
|
end
|
|
59
98
|
|
|
60
99
|
def process_batch(api, batch_size: 50, dry_run: false, delay_sec: 0.15)
|
data/lib/gsc/page_speed.rb
CHANGED
|
@@ -86,6 +86,29 @@ module GSC
|
|
|
86
86
|
|
|
87
87
|
overall_category = data.dig('loadingExperience', 'overall_category') || data.dig('originLoadingExperience', 'overall_category')
|
|
88
88
|
|
|
89
|
+
# Official Google Core Web Vitals Assessment evaluates ONLY the 3 Core Web Vitals:
|
|
90
|
+
# LCP, INP (or legacy FID), and CLS. (FCP and TTFB are diagnostic metrics).
|
|
91
|
+
# - PASSED: All present CWV metrics are 'FAST' (Good).
|
|
92
|
+
# - POOR: Any CWV metric is 'SLOW' (Poor).
|
|
93
|
+
# - NEEDS IMPROVEMENT: No CWV metric is 'SLOW', but at least one is 'AVERAGE'.
|
|
94
|
+
cwv_keys = ['LARGEST_CONTENTFUL_PAINT_MS', 'INTERACTION_TO_NEXT_PAINT', 'CUMULATIVE_LAYOUT_SHIFT_SCORE']
|
|
95
|
+
cwv_keys[1] = 'FIRST_INPUT_DELAY_MS' if !crux_metrics.key?('INTERACTION_TO_NEXT_PAINT') && crux_metrics.key?('FIRST_INPUT_DELAY_MS')
|
|
96
|
+
|
|
97
|
+
active_cwv = cwv_keys.map { |k| crux_metrics[k] }.compact
|
|
98
|
+
cwv_assessment = if active_cwv.any?
|
|
99
|
+
if active_cwv.any? { |m| m[:category] == 'SLOW' }
|
|
100
|
+
'POOR'
|
|
101
|
+
elsif active_cwv.any? { |m| m[:category] == 'AVERAGE' }
|
|
102
|
+
'NEEDS IMPROVEMENT'
|
|
103
|
+
elsif active_cwv.all? { |m| m[:category] == 'FAST' }
|
|
104
|
+
'PASSED'
|
|
105
|
+
else
|
|
106
|
+
overall_category
|
|
107
|
+
end
|
|
108
|
+
else
|
|
109
|
+
overall_category
|
|
110
|
+
end
|
|
111
|
+
|
|
89
112
|
# Opportunities
|
|
90
113
|
opportunities = []
|
|
91
114
|
audits.each do |k, v|
|
|
@@ -117,6 +140,7 @@ module GSC
|
|
|
117
140
|
field_data: crux_metrics,
|
|
118
141
|
field_source: crux_source,
|
|
119
142
|
overall_category: overall_category,
|
|
143
|
+
cwv_assessment: cwv_assessment,
|
|
120
144
|
opportunities: opportunities.first(5)
|
|
121
145
|
}
|
|
122
146
|
end
|
data/lib/gsc/version.rb
CHANGED