gsc-cli 2.2.4 → 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 +117 -11
- data/dist/gsc +117 -11
- data/lib/gsc/cli/indexing.rb +53 -4
- data/lib/gsc/cli.rb +12 -0
- 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/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
|
|
|
@@ -5429,13 +5435,16 @@ require 'time'
|
|
|
5429
5435
|
module GSC
|
|
5430
5436
|
class IndexingQueue
|
|
5431
5437
|
QUEUE_FILE = File.join(Config::CONFIG_DIR, 'indexing_queue.json')
|
|
5438
|
+
BACKUPS_DIR = File.join(Config::CONFIG_DIR, 'backups')
|
|
5432
5439
|
DAILY_LIMIT = 200
|
|
5433
5440
|
|
|
5434
|
-
attr_reader :state
|
|
5441
|
+
attr_reader :state, :file_path, :backup_dir
|
|
5435
5442
|
|
|
5436
|
-
def initialize(file_path = QUEUE_FILE)
|
|
5443
|
+
def initialize(file_path = QUEUE_FILE, backup_dir: nil)
|
|
5437
5444
|
@file_path = file_path
|
|
5445
|
+
@backup_dir = backup_dir || File.join(File.dirname(@file_path), 'backups')
|
|
5438
5446
|
@state = load_state
|
|
5447
|
+
Config.ensure_gsc_symlink!
|
|
5439
5448
|
end
|
|
5440
5449
|
|
|
5441
5450
|
def add_urls(urls)
|
|
@@ -5467,7 +5476,41 @@ module GSC
|
|
|
5467
5476
|
}
|
|
5468
5477
|
end
|
|
5469
5478
|
|
|
5470
|
-
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
|
+
|
|
5471
5514
|
if scope == :pending
|
|
5472
5515
|
@state['pending'] = []
|
|
5473
5516
|
else
|
|
@@ -5476,6 +5519,8 @@ module GSC
|
|
|
5476
5519
|
@state['failed'] = []
|
|
5477
5520
|
end
|
|
5478
5521
|
save_state
|
|
5522
|
+
|
|
5523
|
+
{ count: count, backup_file: backup_file }
|
|
5479
5524
|
end
|
|
5480
5525
|
|
|
5481
5526
|
def process_batch(api, batch_size: 50, dry_run: false, delay_sec: 0.15)
|
|
@@ -9063,7 +9108,7 @@ COMMAND_REGISTRY = [
|
|
|
9063
9108
|
commands: [
|
|
9064
9109
|
{ name: "inspect <url>", shortcut: nil, desc: "Live Google index check (coverage, canonical, date, robots.txt)", flags: ["-d"] },
|
|
9065
9110
|
{ name: "index <url>", shortcut: nil, desc: "Notify Googlebot to crawl/index URL immediately (URL_UPDATED)", flags: ["--dry-run"] },
|
|
9066
|
-
{ 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"] },
|
|
9067
9112
|
{ name: "remove <url>", shortcut: nil, desc: "Notify Googlebot a page has been deleted (URL_DELETED)", flags: ["--dry-run"] },
|
|
9068
9113
|
{ name: "status <url>", shortcut: nil, desc: "Check Google Indexing API notification metadata", flags: [] },
|
|
9069
9114
|
{ name: "sitemaps-list", shortcut: nil, desc: "List registered XML sitemaps in Search Console", flags: ["-d"] },
|
|
@@ -21034,11 +21079,54 @@ module GSC
|
|
|
21034
21079
|
end
|
|
21035
21080
|
|
|
21036
21081
|
when 'clear', 'reset'
|
|
21037
|
-
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
|
+
|
|
21038
21121
|
if options[:json]
|
|
21039
|
-
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
|
+
})
|
|
21040
21128
|
else
|
|
21041
|
-
puts
|
|
21129
|
+
puts "Queue cleared (#{cleared_count} items). Backup saved to #{display_backup}"
|
|
21042
21130
|
end
|
|
21043
21131
|
|
|
21044
21132
|
else
|
|
@@ -21056,7 +21144,13 @@ module GSC
|
|
|
21056
21144
|
puts "\nCommands:"
|
|
21057
21145
|
puts " • #{Color.c('gsc index-batch add <url|sitemap.xml|file.txt>', Color::CYAN)} - Enqueue URLs"
|
|
21058
21146
|
puts " • #{Color.c('gsc index-batch run [--batch-size 50] [--dry-run]', Color::CYAN)} - Process queue"
|
|
21059
|
-
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"
|
|
21060
21154
|
puts ""
|
|
21061
21155
|
end
|
|
21062
21156
|
end
|
|
@@ -31571,6 +31665,10 @@ module GSC
|
|
|
31571
31665
|
options[:batch_size] = size
|
|
31572
31666
|
end
|
|
31573
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
|
+
|
|
31574
31672
|
opts.on('--concurrency COUNT', Integer, 'Concurrent threads for multi-page site crawler (default: 5, max: 20)') do |c|
|
|
31575
31673
|
options[:concurrency] = c
|
|
31576
31674
|
end
|
|
@@ -32227,6 +32325,14 @@ module GSC
|
|
|
32227
32325
|
Audit.run(command, target, extra, options)
|
|
32228
32326
|
exit 0 unless options[:in_dashboard]
|
|
32229
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
|
|
32230
32336
|
end
|
|
32231
32337
|
|
|
32232
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
|
|
|
@@ -5429,13 +5435,16 @@ require 'time'
|
|
|
5429
5435
|
module GSC
|
|
5430
5436
|
class IndexingQueue
|
|
5431
5437
|
QUEUE_FILE = File.join(Config::CONFIG_DIR, 'indexing_queue.json')
|
|
5438
|
+
BACKUPS_DIR = File.join(Config::CONFIG_DIR, 'backups')
|
|
5432
5439
|
DAILY_LIMIT = 200
|
|
5433
5440
|
|
|
5434
|
-
attr_reader :state
|
|
5441
|
+
attr_reader :state, :file_path, :backup_dir
|
|
5435
5442
|
|
|
5436
|
-
def initialize(file_path = QUEUE_FILE)
|
|
5443
|
+
def initialize(file_path = QUEUE_FILE, backup_dir: nil)
|
|
5437
5444
|
@file_path = file_path
|
|
5445
|
+
@backup_dir = backup_dir || File.join(File.dirname(@file_path), 'backups')
|
|
5438
5446
|
@state = load_state
|
|
5447
|
+
Config.ensure_gsc_symlink!
|
|
5439
5448
|
end
|
|
5440
5449
|
|
|
5441
5450
|
def add_urls(urls)
|
|
@@ -5467,7 +5476,41 @@ module GSC
|
|
|
5467
5476
|
}
|
|
5468
5477
|
end
|
|
5469
5478
|
|
|
5470
|
-
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
|
+
|
|
5471
5514
|
if scope == :pending
|
|
5472
5515
|
@state['pending'] = []
|
|
5473
5516
|
else
|
|
@@ -5476,6 +5519,8 @@ module GSC
|
|
|
5476
5519
|
@state['failed'] = []
|
|
5477
5520
|
end
|
|
5478
5521
|
save_state
|
|
5522
|
+
|
|
5523
|
+
{ count: count, backup_file: backup_file }
|
|
5479
5524
|
end
|
|
5480
5525
|
|
|
5481
5526
|
def process_batch(api, batch_size: 50, dry_run: false, delay_sec: 0.15)
|
|
@@ -9063,7 +9108,7 @@ COMMAND_REGISTRY = [
|
|
|
9063
9108
|
commands: [
|
|
9064
9109
|
{ name: "inspect <url>", shortcut: nil, desc: "Live Google index check (coverage, canonical, date, robots.txt)", flags: ["-d"] },
|
|
9065
9110
|
{ name: "index <url>", shortcut: nil, desc: "Notify Googlebot to crawl/index URL immediately (URL_UPDATED)", flags: ["--dry-run"] },
|
|
9066
|
-
{ 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"] },
|
|
9067
9112
|
{ name: "remove <url>", shortcut: nil, desc: "Notify Googlebot a page has been deleted (URL_DELETED)", flags: ["--dry-run"] },
|
|
9068
9113
|
{ name: "status <url>", shortcut: nil, desc: "Check Google Indexing API notification metadata", flags: [] },
|
|
9069
9114
|
{ name: "sitemaps-list", shortcut: nil, desc: "List registered XML sitemaps in Search Console", flags: ["-d"] },
|
|
@@ -21034,11 +21079,54 @@ module GSC
|
|
|
21034
21079
|
end
|
|
21035
21080
|
|
|
21036
21081
|
when 'clear', 'reset'
|
|
21037
|
-
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
|
+
|
|
21038
21121
|
if options[:json]
|
|
21039
|
-
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
|
+
})
|
|
21040
21128
|
else
|
|
21041
|
-
puts
|
|
21129
|
+
puts "Queue cleared (#{cleared_count} items). Backup saved to #{display_backup}"
|
|
21042
21130
|
end
|
|
21043
21131
|
|
|
21044
21132
|
else
|
|
@@ -21056,7 +21144,13 @@ module GSC
|
|
|
21056
21144
|
puts "\nCommands:"
|
|
21057
21145
|
puts " • #{Color.c('gsc index-batch add <url|sitemap.xml|file.txt>', Color::CYAN)} - Enqueue URLs"
|
|
21058
21146
|
puts " • #{Color.c('gsc index-batch run [--batch-size 50] [--dry-run]', Color::CYAN)} - Process queue"
|
|
21059
|
-
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"
|
|
21060
21154
|
puts ""
|
|
21061
21155
|
end
|
|
21062
21156
|
end
|
|
@@ -31571,6 +31665,10 @@ module GSC
|
|
|
31571
31665
|
options[:batch_size] = size
|
|
31572
31666
|
end
|
|
31573
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
|
+
|
|
31574
31672
|
opts.on('--concurrency COUNT', Integer, 'Concurrent threads for multi-page site crawler (default: 5, max: 20)') do |c|
|
|
31575
31673
|
options[:concurrency] = c
|
|
31576
31674
|
end
|
|
@@ -32227,6 +32325,14 @@ module GSC
|
|
|
32227
32325
|
Audit.run(command, target, extra, options)
|
|
32228
32326
|
exit 0 unless options[:in_dashboard]
|
|
32229
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
|
|
32230
32336
|
end
|
|
32231
32337
|
|
|
32232
32338
|
# 7. Authenticated Commands (GSC / Indexing / Analytics / GA4)
|
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/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/version.rb
CHANGED