rogiq 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/exe/rogiq +49 -0
  3. data/lib/rogiq/cli.rb +51 -0
  4. data/lib/rogiq/commands/accounts.rb +114 -0
  5. data/lib/rogiq/commands/ai.rb +71 -0
  6. data/lib/rogiq/commands/auth.rb +194 -0
  7. data/lib/rogiq/commands/base.rb +26 -0
  8. data/lib/rogiq/commands/billing.rb +90 -0
  9. data/lib/rogiq/commands/billing_stripe.rb +15 -0
  10. data/lib/rogiq/commands/clients.rb +117 -0
  11. data/lib/rogiq/commands/content.rb +71 -0
  12. data/lib/rogiq/commands/diagnose.rb +134 -0
  13. data/lib/rogiq/commands/jobs.rb +126 -0
  14. data/lib/rogiq/commands/queue.rb +115 -0
  15. data/lib/rogiq/commands/security.rb +33 -0
  16. data/lib/rogiq/commands/seo.rb +49 -0
  17. data/lib/rogiq/commands/seo_audit.rb +51 -0
  18. data/lib/rogiq/commands/status.rb +166 -0
  19. data/lib/rogiq/commands/sync.rb +60 -0
  20. data/lib/rogiq/config_store.rb +59 -0
  21. data/lib/rogiq/formatters.rb +103 -0
  22. data/lib/rogiq/helpers.rb +53 -0
  23. data/lib/rogiq/http_api.rb +81 -0
  24. data/lib/rogiq/rails_loader.rb +22 -0
  25. data/lib/rogiq/remote/accounts.rb +38 -0
  26. data/lib/rogiq/remote/ai.rb +50 -0
  27. data/lib/rogiq/remote/billing.rb +39 -0
  28. data/lib/rogiq/remote/clients.rb +44 -0
  29. data/lib/rogiq/remote/command_base.rb +41 -0
  30. data/lib/rogiq/remote/content.rb +37 -0
  31. data/lib/rogiq/remote/diagnose.rb +24 -0
  32. data/lib/rogiq/remote/jobs.rb +52 -0
  33. data/lib/rogiq/remote/queue.rb +72 -0
  34. data/lib/rogiq/remote/security.rb +26 -0
  35. data/lib/rogiq/remote/seo.rb +32 -0
  36. data/lib/rogiq/remote/status.rb +35 -0
  37. data/lib/rogiq/remote/sync.rb +49 -0
  38. data/lib/rogiq/remote_cli.rb +73 -0
  39. data/lib/rogiq/version.rb +5 -0
  40. metadata +96 -0
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RogIQ
4
+ module Commands
5
+ class Clients < RogIQ::Commands::Base
6
+ desc "list", "List recent clients"
7
+ method_option :limit, type: :numeric, default: 25, aliases: "-n", desc: "Max records"
8
+ method_option :search, type: :string, desc: "Filter by name (ILIKE)"
9
+ def list
10
+ RogIQ.load_rails!
11
+ scope = ::Client.order(created_at: :desc)
12
+ scope = scope.where("name ILIKE ?", "%#{options[:search]}%") if options[:search].present?
13
+ rows = scope.limit(options[:limit]).map do |c|
14
+ slug = c.account&.slug
15
+ [c.id, c.name, slug, c.research_ready, c.created_at.iso8601]
16
+ end
17
+ fmt.output(headers: %w[id name account_slug research_ready created_at], rows: rows)
18
+ end
19
+
20
+ desc "show IDENTIFIER", "Show client by uuid or name"
21
+ def show(identifier)
22
+ RogIQ.load_rails!
23
+ c = RogIQ::Helpers.resolve_client(identifier)
24
+ unless c
25
+ fmt.error_msg("Client not found: #{identifier}")
26
+ exit 1
27
+ end
28
+
29
+ payload = {
30
+ id: c.id,
31
+ name: c.name,
32
+ account_id: c.account_id,
33
+ account_slug: c.account&.slug,
34
+ website: c.website,
35
+ industry: c.industry,
36
+ research_ready: c.research_ready,
37
+ onboarding_stage: c.try(:onboarding_stage),
38
+ onboarding_version: c.try(:onboarding_version),
39
+ created_at: c.created_at,
40
+ updated_at: c.updated_at
41
+ }
42
+ fmt.output(payload)
43
+ end
44
+
45
+ desc "research_status IDENTIFIER", "Print client_settings research_status JSON"
46
+ map "research-status" => :research_status
47
+ def research_status(identifier)
48
+ RogIQ.load_rails!
49
+ c = RogIQ::Helpers.resolve_client(identifier)
50
+ unless c
51
+ fmt.error_msg("Client not found")
52
+ exit 1
53
+ end
54
+
55
+ row = c.client_settings.find_by(setting_key: "research_status")
56
+ fmt.output(row ? row.setting_value : {})
57
+ end
58
+
59
+ desc "settings IDENTIFIER", "List all client_settings rows"
60
+ def settings(identifier)
61
+ RogIQ.load_rails!
62
+ c = RogIQ::Helpers.resolve_client(identifier)
63
+ unless c
64
+ fmt.error_msg("Client not found")
65
+ exit 1
66
+ end
67
+
68
+ rows = c.client_settings.order(:setting_key).map do |s|
69
+ val = s.setting_value
70
+ preview = val.is_a?(Hash) ? val.to_json.truncate(120) : val.to_s.truncate(120)
71
+ [s.setting_key, preview]
72
+ end
73
+ fmt.output(headers: %w[setting_key value_preview], rows: rows)
74
+ end
75
+
76
+ desc "onboarding IDENTIFIER", "Latest onboarding evaluation / version summary"
77
+ def onboarding(identifier)
78
+ RogIQ.load_rails!
79
+ c = RogIQ::Helpers.resolve_client(identifier)
80
+ unless c
81
+ fmt.error_msg("Client not found")
82
+ exit 1
83
+ end
84
+
85
+ ev = c.onboarding_evaluations.order(created_at: :desc).first
86
+ ver = c.onboarding_versions.order(created_at: :desc).first
87
+ fmt.output(
88
+ {
89
+ client_id: c.id,
90
+ latest_evaluation: ev&.attributes,
91
+ latest_version: ver&.attributes
92
+ }.compact
93
+ )
94
+ end
95
+
96
+ desc "subdomain IDENTIFIER", "Account slug (white-label) + custom domains if any"
97
+ def subdomain(identifier)
98
+ RogIQ.load_rails!
99
+ c = RogIQ::Helpers.resolve_client(identifier)
100
+ unless c
101
+ fmt.error_msg("Client not found")
102
+ exit 1
103
+ end
104
+
105
+ acc = c.account
106
+ domains = acc ? ::CustomDomain.where(account_id: acc.id).limit(50).pluck(:domain, :status, :verified_at) : []
107
+ fmt.output(
108
+ {
109
+ account_slug: acc&.slug,
110
+ account_id: acc&.id,
111
+ custom_domains: domains.map { |d, st, v| { domain: d, status: st, verified_at: v } }
112
+ }
113
+ )
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RogIQ
4
+ module Commands
5
+ class Content < RogIQ::Commands::Base
6
+ desc "status CLIENT", "ContentItem counts by state"
7
+ def status(client_identifier)
8
+ RogIQ.load_rails!
9
+ client = RogIQ::Helpers.resolve_client(client_identifier)
10
+ unless client
11
+ fmt.error_msg("Client not found")
12
+ exit 1
13
+ end
14
+
15
+ counts = ::ContentItem.where(client_id: client.id).group(:state).count.sort_by { |_, n| -n }
16
+ fmt.output(headers: %w[state count], rows: counts.to_a)
17
+ end
18
+
19
+ desc "heal CLIENT", "Enqueue DataHealingOrchestratorJob"
20
+ def heal(client_identifier)
21
+ RogIQ.load_rails!
22
+ client = RogIQ::Helpers.resolve_client(client_identifier)
23
+ unless client
24
+ fmt.error_msg("Client not found")
25
+ exit 1
26
+ end
27
+
28
+ ::DataHealingOrchestratorJob.perform_later(client_id: client.id, account_id: client.account_id)
29
+ fmt.success("DataHealingOrchestratorJob enqueued for client #{client.id}.")
30
+ end
31
+
32
+ desc "publish_queue", "Scheduled content publications (future)"
33
+ map "publish-queue" => :publish_queue
34
+ method_option :limit, type: :numeric, default: 40
35
+ def publish_queue
36
+ RogIQ.load_rails!
37
+ rows = ::ContentPublication.where(status: "scheduled")
38
+ .where("scheduled_at > ?", Time.current)
39
+ .order(:scheduled_at)
40
+ .limit(options[:limit])
41
+ .map do |p|
42
+ [
43
+ p.id,
44
+ p.account_id,
45
+ p.title.to_s.truncate(50),
46
+ p.scheduled_at&.iso8601,
47
+ p.platform
48
+ ]
49
+ end
50
+ fmt.output(headers: %w[id account_id title scheduled_at platform], rows: rows)
51
+ end
52
+
53
+ desc "failed CLIENT", "Content items in failed or rejected state"
54
+ method_option :limit, type: :numeric, default: 30
55
+ def failed(client_identifier)
56
+ RogIQ.load_rails!
57
+ client = RogIQ::Helpers.resolve_client(client_identifier)
58
+ unless client
59
+ fmt.error_msg("Client not found")
60
+ exit 1
61
+ end
62
+
63
+ rows = ::ContentItem.where(client_id: client.id, state: %w[failed rejected])
64
+ .order(updated_at: :desc)
65
+ .limit(options[:limit])
66
+ .map { |i| [i.id, i.title.to_s.truncate(60), i.state, i.updated_at.iso8601] }
67
+ fmt.output(headers: %w[id title state updated_at], rows: rows)
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RogIQ
4
+ module Commands
5
+ class Diagnose < RogIQ::Commands::Base
6
+ desc "search_insights [CLIENT]", "Run diagnose_search_insights.rb (global DB report; client optional)"
7
+ map "search-insights" => :search_insights
8
+ def search_insights(*)
9
+ RogIQ.load_rails!
10
+ load File.join(RogIQ.repo_root, "diagnose_search_insights.rb")
11
+ end
12
+
13
+ desc "genius CLIENT", "Run debug_genius_mode_goals.rb for a client id/name"
14
+ def genius(identifier)
15
+ RogIQ.load_rails!
16
+ client = RogIQ::Helpers.resolve_client(identifier)
17
+ unless client
18
+ fmt.error_msg("Client not found")
19
+ exit 1
20
+ end
21
+
22
+ RogIQ::Helpers.with_saved_argv([ client.id.to_s ]) do
23
+ load File.join(RogIQ.repo_root, "debug_genius_mode_goals.rb")
24
+ end
25
+ end
26
+
27
+ desc "subdomain CLIENT", "DNS/API checks using account slug (diagnose_subdomain_issues.rb)"
28
+ def subdomain(identifier)
29
+ RogIQ.load_rails!
30
+ client = RogIQ::Helpers.resolve_client(identifier)
31
+ unless client&.account&.slug
32
+ fmt.error_msg("Client or account slug not found")
33
+ exit 1
34
+ end
35
+
36
+ RogIQ::Helpers.with_saved_argv([ client.account.slug ]) do
37
+ load File.join(RogIQ.repo_root, "diagnose_subdomain_issues.rb")
38
+ end
39
+ end
40
+
41
+ desc "emergency CLIENT", "Emergency client diagnostic (jobs, research status, settings)"
42
+ def emergency(identifier)
43
+ RogIQ.load_rails!
44
+ client = RogIQ::Helpers.resolve_client(identifier)
45
+ unless client
46
+ fmt.error_msg("Client not found")
47
+ exit 1
48
+ end
49
+
50
+ fmt.say("\n" + "=" * 80)
51
+ fmt.say("EMERGENCY DIAGNOSTIC: #{client.name} (#{client.id})")
52
+ fmt.say("=" * 80)
53
+
54
+ fmt.say("\n1. CLIENT DATA")
55
+ fmt.say(" Name: #{client.name}")
56
+ fmt.say(" Website: #{client.website.presence || "EMPTY"}")
57
+ fmt.say(" Research Ready: #{client.research_ready}")
58
+ fmt.say(" Description: #{client.description.present? ? client.description[0, 100] + "…" : "EMPTY"}")
59
+ fmt.say(" Primary Color: #{client.primary_color || "EMPTY"}")
60
+ fmt.say(" Created: #{client.created_at} (#{((Time.current - client.created_at) / 60).round} min ago)")
61
+
62
+ fmt.say("\n2. RESEARCH STATUS")
63
+ research = client.client_settings.find_by(setting_key: "research_status")
64
+ if research
65
+ v = research.setting_value
66
+ fmt.say(" Status: #{v["status"]}")
67
+ fmt.say(" Website: #{v["website"] || "none"}")
68
+ fmt.say(" Updated: #{v["updated_at"]}")
69
+ else
70
+ fmt.say(" NO RESEARCH STATUS — after_create callback may not have run")
71
+ end
72
+
73
+ fmt.say("\n3. SOLID QUEUE JOBS FOR THIS CLIENT")
74
+ jobs = ::SolidQueue::Job.where("arguments::text LIKE ?", "%#{client.id}%").order(created_at: :desc)
75
+ if jobs.empty?
76
+ fmt.say(" NO JOBS FOUND — callback not firing, SolidQueue not configured, or DB issue")
77
+ else
78
+ fmt.say(" Found #{jobs.count} job record(s):")
79
+ jobs.group_by(&:class_name).each do |name, list|
80
+ fmt.say(" #{name} (#{list.size} run(s)):")
81
+ list.first(3).each do |job|
82
+ status = job.finished_at ? "FINISHED" : (job.failed_at ? "FAILED" : "PENDING")
83
+ fmt.say(" #{status} — created #{job.created_at}, finished #{job.finished_at || "—"}")
84
+ end
85
+ end
86
+ end
87
+
88
+ fmt.say("\n4. KEY SETTINGS PRESENCE")
89
+ important = %w[website_content ai_insights onboarding_processed_data]
90
+ present = client.client_settings.pluck(:setting_key)
91
+ important.each do |key|
92
+ fmt.say(" #{key}: #{present.include?(key) ? "present" : "MISSING"}")
93
+ end
94
+
95
+ fmt.say("\n" + "=" * 80)
96
+ fmt.say("END DIAGNOSTIC")
97
+ fmt.say("=" * 80)
98
+ end
99
+
100
+ desc "zeitwerk", "Run zeitwerk:check with RAILS_ENV=production"
101
+ def zeitwerk
102
+ api = RogIQ.api_root
103
+ ok = system({ "RAILS_ENV" => "production" }, "bundle", "exec", "rails", "zeitwerk:check", chdir: api)
104
+ exit(ok ? 0 : 1)
105
+ end
106
+
107
+ desc "pinecone", "Pinecone configuration summary"
108
+ def pinecone
109
+ RogIQ.load_rails!
110
+ if defined?(Rails.configuration.ai_engine)
111
+ cfg = Rails.configuration.ai_engine
112
+ fmt.output(
113
+ pinecone_enabled: cfg.try(:pinecone_enabled),
114
+ pinecone_index_name: cfg.try(:pinecone_index_name),
115
+ api_key_present: ENV["PINECONE_API_KEY"].present?
116
+ )
117
+ else
118
+ fmt.output(pinecone: "Rails.configuration.ai_engine not defined")
119
+ end
120
+ end
121
+
122
+ desc "migrations", "Pending migrations?"
123
+ def migrations
124
+ RogIQ.load_rails!
125
+ pending = ActiveRecord::Base.connection.pool.migration_context.pending_migration_versions
126
+ fmt.output(
127
+ pending_count: pending.size,
128
+ pending_versions: pending
129
+ )
130
+ exit(pending.any? ? 1 : 0)
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RogIQ
4
+ module Commands
5
+ class Jobs < RogIQ::Commands::Base
6
+ desc "list", "Summarize Solid Queue job states"
7
+ method_option :failed, type: :boolean, default: false, desc: "Only failed executions (recent)"
8
+ method_option :class, type: :string, desc: "Filter by Active Job class name"
9
+ method_option :limit, type: :numeric, default: 50, desc: "Max rows for --failed list"
10
+ def list
11
+ RogIQ.load_rails!
12
+ unless ActiveRecord::Base.connection.data_source_exists?("solid_queue_jobs")
13
+ fmt.error_msg("solid_queue tables not present")
14
+ exit 1
15
+ end
16
+
17
+ if options[:failed]
18
+ scope = SolidQueue::FailedExecution.joins(:job).order("solid_queue_failed_executions.id DESC")
19
+ scope = scope.where(solid_queue_jobs: { class_name: options[:class] }) if options[:class].present?
20
+ rows = scope.limit(options[:limit]).map do |fe|
21
+ job = fe.job
22
+ msg = fe.error.is_a?(Hash) ? fe.error["message"] : fe.error.to_s
23
+ [fe.id, job&.class_name, msg.to_s.truncate(80)]
24
+ end
25
+ fmt.output(headers: %w[failed_execution_id class_name error_preview], rows: rows)
26
+ else
27
+ unfinished = SolidQueue::Job.where(finished_at: nil).group(:class_name).count.sort_by { |_, n| -n }
28
+ fmt.output(
29
+ headers: %w[class_name unfinished_count],
30
+ rows: unfinished
31
+ )
32
+ fmt.say("") unless options[:quiet]
33
+ fmt.output(
34
+ {
35
+ ready: SolidQueue::ReadyExecution.count,
36
+ scheduled: SolidQueue::ScheduledExecution.count,
37
+ failed: SolidQueue::FailedExecution.count,
38
+ claimed: SolidQueue::ClaimedExecution.count
39
+ }
40
+ )
41
+ end
42
+ end
43
+
44
+ desc "detail ID", "Show full error / metadata for a failed execution or job id"
45
+ map "inspect" => :detail
46
+ def detail(id)
47
+ RogIQ.load_rails!
48
+ fe = find_failed_execution(id)
49
+ unless fe
50
+ fmt.error_msg("Failed execution not found for id=#{id}")
51
+ exit 1
52
+ end
53
+
54
+ job = fe.job
55
+ payload = {
56
+ failed_execution_id: fe.id,
57
+ job_id: job&.id,
58
+ class_name: job&.class_name,
59
+ queue_name: job&.queue_name,
60
+ created_at: job&.created_at,
61
+ error: fe.error,
62
+ arguments: job&.arguments
63
+ }
64
+ fmt.output(payload)
65
+ end
66
+
67
+ desc "retry ID", "Retry a single failed execution (by failed_execution.id or job.id)"
68
+ map "retry" => :retry_one
69
+ def retry_one(id)
70
+ RogIQ.load_rails!
71
+ fe = find_failed_execution(id)
72
+ unless fe
73
+ fmt.error_msg("Failed execution not found for id=#{id}")
74
+ exit 1
75
+ end
76
+ fe.retry
77
+ fmt.success("Retried job #{fe.job_id}")
78
+ end
79
+
80
+ desc "discard ID", "Discard a failed execution without retry"
81
+ map "discard" => :discard_one
82
+ method_option :yes, type: :boolean, default: false, aliases: "-y", desc: "Skip confirmation"
83
+ def discard_one(id)
84
+ RogIQ.load_rails!
85
+ fe = find_failed_execution(id)
86
+ unless fe
87
+ fmt.error_msg("Failed execution not found for id=#{id}")
88
+ exit 1
89
+ end
90
+
91
+ unless fmt.confirm!("Discard failed execution #{fe.id} (job #{fe.job_id})?", yes: options[:yes])
92
+ fmt.warn_msg("Aborted.")
93
+ return
94
+ end
95
+
96
+ fe.discard
97
+ fmt.success("Discarded.")
98
+ end
99
+
100
+ desc "tail", "Poll failed executions every 3 seconds (Ctrl+C to stop)"
101
+ method_option :interval, type: :numeric, default: 3, desc: "Seconds between polls"
102
+ def tail
103
+ RogIQ.load_rails!
104
+ last_id = SolidQueue::FailedExecution.maximum(:id).to_i
105
+ fmt.success("Watching SolidQueue::FailedExecution after id=#{last_id}…")
106
+ loop do
107
+ sleep options[:interval]
108
+ rows = SolidQueue::FailedExecution.where("id > ?", last_id).order(:id)
109
+ rows.each do |fe|
110
+ last_id = fe.id
111
+ job = fe.job
112
+ msg = fe.error.is_a?(Hash) ? fe.error["message"] : fe.error
113
+ fmt.say("[#{fe.id}] #{job&.class_name}: #{msg}")
114
+ end
115
+ end
116
+ end
117
+
118
+ private
119
+
120
+ def find_failed_execution(id)
121
+ SolidQueue::FailedExecution.find_by(id: id) ||
122
+ SolidQueue::FailedExecution.find_by(job_id: id)
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RogIQ
4
+ module Commands
5
+ class Queue < RogIQ::Commands::Base
6
+ desc "stats", "Solid Queue counts (ready, scheduled, failed, claimed)"
7
+ method_option :by_class, type: :boolean, default: false, desc: "Break down unfinished jobs by Active Job class (--by-class)"
8
+ def stats
9
+ RogIQ.load_rails!
10
+ unless solid_queue_tables?
11
+ fmt.error_msg("solid_queue tables not present")
12
+ exit 1
13
+ end
14
+
15
+ if options[:by_class]
16
+ rows = SolidQueue::Job.where(finished_at: nil).group(:class_name).count.sort_by { |_, n| -n }
17
+ fmt.output(headers: %w[class_name unfinished_jobs], rows: rows.to_a)
18
+ else
19
+ payload = {
20
+ ready: SolidQueue::ReadyExecution.count,
21
+ scheduled: SolidQueue::ScheduledExecution.count,
22
+ failed: SolidQueue::FailedExecution.count,
23
+ claimed: SolidQueue::ClaimedExecution.count,
24
+ blocked: defined?(SolidQueue::BlockedExecution) ? SolidQueue::BlockedExecution.count : nil
25
+ }.compact
26
+ fmt.output(payload)
27
+ end
28
+ end
29
+
30
+ desc "flush", "Discard failed executions (--class optional)"
31
+ method_option :class, type: :string, required: false, desc: "Limit to this Active Job class name"
32
+ method_option :yes, type: :boolean, default: false, aliases: "-y", desc: "Skip confirmation"
33
+ def flush
34
+ RogIQ.load_rails!
35
+ scope = SolidQueue::FailedExecution.joins(:job)
36
+ scope = scope.where(solid_queue_jobs: { class_name: options[:class] }) if options[:class].present?
37
+
38
+ n = scope.count
39
+ if n.zero?
40
+ fmt.success("No failed executions to discard.")
41
+ return
42
+ end
43
+
44
+ unless fmt.confirm!("Discard #{n} failed execution(s)?", yes: options[:yes])
45
+ fmt.warn_msg("Aborted.")
46
+ return
47
+ end
48
+
49
+ discarded = 0
50
+ scope.find_each do |fe|
51
+ fe.discard
52
+ discarded += 1
53
+ end
54
+ fmt.success("Discarded #{discarded} failed execution(s).")
55
+ end
56
+
57
+ desc "retry_all", "Retry all failed jobs (optionally scoped with --class)"
58
+ map "retry-all" => :retry_all
59
+ method_option :class, type: :string, required: false, desc: "Only this Active Job class"
60
+ method_option :yes, type: :boolean, default: false, aliases: "-y", desc: "Skip confirmation"
61
+ def retry_all
62
+ RogIQ.load_rails!
63
+ scope = SolidQueue::FailedExecution.joins(:job)
64
+ scope = scope.where(solid_queue_jobs: { class_name: options[:class] }) if options[:class].present?
65
+ jobs = scope.includes(:job).map(&:job).uniq
66
+ if jobs.empty?
67
+ fmt.success("Nothing to retry.")
68
+ return
69
+ end
70
+
71
+ unless fmt.confirm!("Retry #{jobs.size} failed job(s)?", yes: options[:yes])
72
+ fmt.warn_msg("Aborted.")
73
+ return
74
+ end
75
+
76
+ SolidQueue::FailedExecution.retry_all(jobs)
77
+ fmt.success("Retried #{jobs.size} job(s).")
78
+ end
79
+
80
+ desc "pause", "Pause a Solid Queue queue by name"
81
+ def pause(queue_name)
82
+ RogIQ.load_rails!
83
+ SolidQueue::Queue.find_by_name(queue_name).pause
84
+ fmt.success("Paused queue #{queue_name}")
85
+ end
86
+
87
+ desc "resume", "Resume a paused Solid Queue queue"
88
+ def resume(queue_name)
89
+ RogIQ.load_rails!
90
+ SolidQueue::Queue.find_by_name(queue_name).resume
91
+ fmt.success("Resumed queue #{queue_name}")
92
+ end
93
+
94
+ desc "drain", "Clear ready/scheduled/blocked (dangerous; wraps queue:drain rake task semantics)"
95
+ method_option :yes, type: :boolean, default: false, aliases: "-y", desc: "Skip confirmation"
96
+ def drain
97
+ RogIQ.load_rails!
98
+ unless fmt.confirm!("Clear ready, scheduled, and blocked executions?", yes: options[:yes])
99
+ fmt.warn_msg("Aborted.")
100
+ return
101
+ end
102
+
103
+ Rails.application.load_tasks
104
+ Rake::Task["queue:drain"].invoke
105
+ fmt.success("Drain complete.")
106
+ end
107
+
108
+ private
109
+
110
+ def solid_queue_tables?
111
+ ActiveRecord::Base.connection.data_source_exists?("solid_queue_jobs")
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RogIQ
4
+ module Commands
5
+ class Security < RogIQ::Commands::Base
6
+ desc "scan", "Run Brakeman (requires dev/test Gemfile group)"
7
+ def scan
8
+ api = RogIQ.api_root
9
+ brakeman = File.join(api, "bin", "brakeman")
10
+ cmd = File.exist?(brakeman) ? [ brakeman.to_s, "-q" ] : %w[bundle exec brakeman -q]
11
+ ok = system(*cmd, chdir: api)
12
+ unless ok
13
+ fmt.error_msg("Brakeman failed or gem not available in this bundle group.")
14
+ exit 1
15
+ end
16
+ end
17
+
18
+ desc "audit", "Run bundler-audit"
19
+ def audit
20
+ api = RogIQ.api_root
21
+ ok = system("bundle", "exec", "bundle-audit", "check", chdir: api)
22
+ exit(ok ? 0 : 1)
23
+ end
24
+
25
+ desc "policy", "rake security_policy:diagnose"
26
+ def policy
27
+ RogIQ.load_rails!
28
+ Rails.application.load_tasks
29
+ Rake::Task["security_policy:diagnose"].invoke
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RogIQ
4
+ module Commands
5
+ class SEO < RogIQ::Commands::Base
6
+ desc "audit SUBCOMMAND ...", "SEO audit (run, status)"
7
+ subcommand "audit", RogIQ::Commands::SEOAudit
8
+
9
+ desc "keywords CLIENT", "Track keywords for client"
10
+ def keywords(client_identifier)
11
+ RogIQ.load_rails!
12
+ client = RogIQ::Helpers.resolve_client(client_identifier)
13
+ unless client
14
+ fmt.error_msg("Client not found")
15
+ exit 1
16
+ end
17
+
18
+ rows = client.client_seo_track_keywords.order(:sort_order, :keyword).limit(200).map do |k|
19
+ [k.keyword, k.sort_order, k.source]
20
+ end
21
+ fmt.output(headers: %w[keyword sort_order source], rows: rows)
22
+ end
23
+
24
+ desc "insights CLIENT", "Search insights counts"
25
+ def insights(client_identifier)
26
+ RogIQ.load_rails!
27
+ client = RogIQ::Helpers.resolve_client(client_identifier)
28
+ unless client
29
+ fmt.error_msg("Client not found")
30
+ exit 1
31
+ end
32
+
33
+ scope = ::SearchInsight.where(client_id: client.id)
34
+ fmt.output(
35
+ total: scope.count,
36
+ by_status: scope.group(:status).count
37
+ )
38
+ end
39
+
40
+ desc "zeitwerk", "Run zeitwerk:check in production mode"
41
+ def zeitwerk
42
+ fmt.warn_msg("Running zeitwerk:check with RAILS_ENV=production …")
43
+ api = RogIQ.api_root
44
+ ok = system({ "RAILS_ENV" => "production" }, "bundle", "exec", "rails", "zeitwerk:check", chdir: api)
45
+ exit(ok ? 0 : 1)
46
+ end
47
+ end
48
+ end
49
+ end