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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a0154a09e436255ed97202beba5751fe2b9ed58218607b4eb1505874720e3590
4
- data.tar.gz: e80b323d84dc95de1605efa3f87760c8ad3bb2f85f033d4f4a6625e51267fa21
3
+ metadata.gz: b146c585e5fdbbea6d4b61644f052df6e75b418d64666d4b4b6006825f0e6eaf
4
+ data.tar.gz: 04b24385606b02d483f14db0759848890aba51714b2fca731b483d9d88bf3346
5
5
  SHA512:
6
- metadata.gz: f1f6f6b813125ae759e18bc74309fed17bb9f5acc29a2a6f78515b5127332a7dc27b7cad9c2649a821ce5bef7bcc8ba0c3376d6a19243093918732700294a4dc
7
- data.tar.gz: 741bd3fd7fd509ad82836c070a7c5f982f81bf114a99d35935cff4b02d022a214beadd05988043036cae536cc43cf0bd289e40d2cf845387a2c3d38c350e2f79
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 | `--run`, `--status`, `--json` |
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.4)
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.4'
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.get(key)
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 clear(scope = :all)
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.clear(:all)
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({ ok: true, message: 'Queue cleared' })
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 Color.c("🗑️ Indexing queue cleared.", Color::GREEN)
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)} - Reset queue"
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.4)
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.4'
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.get(key)
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 clear(scope = :all)
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.clear(:all)
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({ ok: true, message: 'Queue cleared' })
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 Color.c("🗑️ Indexing queue cleared.", Color::GREEN)
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)} - Reset queue"
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)
@@ -226,11 +226,54 @@ module GSC
226
226
  end
227
227
 
228
228
  when 'clear', 'reset'
229
- queue.clear(:all)
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({ ok: true, message: 'Queue cleared' })
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 Color.c("🗑️ Indexing queue cleared.", Color::GREEN)
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)} - Reset queue"
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)
@@ -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.get(key)
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
 
@@ -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 clear(scope = :all)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GSC
4
- VERSION = '2.2.4'
4
+ VERSION = '2.2.5'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gsc-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.4
4
+ version: 2.2.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - ApollosWave LLC