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,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RogIQ
4
+ module Commands
5
+ class SEOAudit < RogIQ::Commands::Base
6
+ desc "run CLIENT", "Create audit run + enqueue SeoModule::AuditKickoffJob"
7
+ map run: :kickoff
8
+ def kickoff(client_identifier)
9
+ RogIQ.load_rails!
10
+ client = RogIQ::Helpers.resolve_client(client_identifier)
11
+ unless client
12
+ fmt.error_msg("Client not found")
13
+ exit 1
14
+ end
15
+
16
+ settings = ::ClientSeoModuleSetting.find_or_create_by!(client_id: client.id)
17
+ seed = client.website.presence
18
+ if seed.blank?
19
+ fmt.error_msg("Client needs a website URL for audit seed")
20
+ exit 1
21
+ end
22
+
23
+ audit_run = client.client_seo_module_audit_runs.create!(
24
+ status: "pending",
25
+ max_pages: settings.crawl_max_pages,
26
+ seed_url: seed.to_s.strip,
27
+ started_at: Time.current
28
+ )
29
+ ::SeoModule::AuditKickoffJob.perform_later(audit_run.id)
30
+ fmt.success("Audit run #{audit_run.id} enqueued.")
31
+ fmt.output(id: audit_run.id, status: audit_run.status, seed_url: audit_run.seed_url)
32
+ end
33
+
34
+ desc "status CLIENT", "Latest SEO module audit runs"
35
+ def status(client_identifier)
36
+ RogIQ.load_rails!
37
+ client = RogIQ::Helpers.resolve_client(client_identifier)
38
+ unless client
39
+ fmt.error_msg("Client not found")
40
+ exit 1
41
+ end
42
+
43
+ runs = client.client_seo_module_audit_runs.order(created_at: :desc).limit(15)
44
+ rows = runs.map do |r|
45
+ [r.id, r.status, r.seed_url.to_s.truncate(60), r.created_at.iso8601]
46
+ end
47
+ fmt.output(headers: %w[id status seed_url created_at], rows: rows)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+
6
+ module RogIQ
7
+ module Commands
8
+ class Status < RogIQ::Commands::Base
9
+ default_task :dashboard
10
+
11
+ desc "dashboard", "System health (default: full dashboard)"
12
+ method_option :ping, type: :boolean, default: false, desc: "HTTP GET /up only (no Rails boot)"
13
+ method_option :queues, type: :boolean, default: false, desc: "Solid Queue breakdown by queue name"
14
+ method_option :db, type: :boolean, default: false, desc: "Database connectivity + schema version"
15
+ method_option :redis, type: :boolean, default: false, desc: "Redis ping (REDIS_URL)"
16
+ method_option :ai, type: :boolean, default: false, desc: "AI provider env configuration"
17
+ method_option :host, type: :string, default: nil, desc: "Base URL for --ping (default ROGIQ_HEALTH_URL or http://127.0.0.1:3000)"
18
+
19
+ def dashboard
20
+ return ping_only if options[:ping]
21
+
22
+ subset = options[:queues] || options[:db] || options[:redis] || options[:ai]
23
+ return dashboard_partial if subset
24
+
25
+ dashboard_full
26
+ end
27
+
28
+ private
29
+
30
+ def ping_only
31
+ host_opt = options[:host].to_s.strip
32
+ base = if host_opt.empty?
33
+ ENV.fetch("ROGIQ_HEALTH_URL", "http://127.0.0.1:3000").chomp("/")
34
+ else
35
+ host_opt.chomp("/")
36
+ end
37
+ uri = URI("#{base}/up")
38
+ http = Net::HTTP.new(uri.host, uri.port)
39
+ http.use_ssl = uri.scheme == "https"
40
+ http.open_timeout = 3
41
+ http.read_timeout = 5
42
+ req_path = uri.path.to_s
43
+ req_path = "/" if req_path.empty?
44
+ res = http.request_get(req_path)
45
+ payload = { url: uri.to_s, code: res.code, body: res.body.to_s[0, 200] }
46
+ fmt.output(payload)
47
+ exit(res.code.to_i >= 400 ? 1 : 0)
48
+ rescue StandardError => e
49
+ fmt.error_msg("Ping failed: #{e.message}")
50
+ exit 1
51
+ end
52
+
53
+ def dashboard_partial
54
+ RogIQ.load_rails!
55
+ rows = []
56
+ headers = %w[check status detail]
57
+
58
+ if options[:queues]
59
+ rows.concat(queue_rows)
60
+ end
61
+ if options[:db]
62
+ rows << db_row
63
+ end
64
+ if options[:redis]
65
+ rows << redis_row
66
+ end
67
+ if options[:ai]
68
+ rows << ai_row
69
+ end
70
+
71
+ fmt.output(headers: headers, rows: rows)
72
+ end
73
+
74
+ def dashboard_full
75
+ RogIQ.load_rails!
76
+ headers = %w[check status detail]
77
+ rows = []
78
+ rows << db_row
79
+ rows << redis_row
80
+ rows << ai_row
81
+ rows << queue_summary_row
82
+ rows.concat(queue_rows) if solid_queue_tables?
83
+
84
+ fmt.output(headers: headers, rows: rows)
85
+
86
+ host = ENV.fetch("ROGIQ_HEALTH_URL", "http://127.0.0.1:3000").chomp("/")
87
+ ping_http("#{host}/health", "HTTP /health")
88
+ ping_http("#{host}/up", "HTTP /up")
89
+ end
90
+
91
+ def ping_http(url, label)
92
+ uri = URI(url)
93
+ http = Net::HTTP.new(uri.host, uri.port)
94
+ http.use_ssl = uri.scheme == "https"
95
+ http.open_timeout = 2
96
+ http.read_timeout = 3
97
+ res = http.request_get(uri.path)
98
+ fmt.say("#{label}: #{res.code}")
99
+ rescue StandardError => e
100
+ fmt.warn_msg("#{label}: unreachable (#{e.message})")
101
+ end
102
+
103
+ def db_row
104
+ ver = ActiveRecord::Base.connection.select_value(
105
+ "SELECT MAX(version) FROM schema_migrations"
106
+ )
107
+ ["database", "ok", "migration=#{ver}"]
108
+ rescue StandardError => e
109
+ ["database", "error", e.message.to_s[0, 120]]
110
+ end
111
+
112
+ def redis_row
113
+ url = ENV["REDIS_URL"].presence
114
+ return ["redis", "skipped", "REDIS_URL not set"] unless url
115
+
116
+ require "redis"
117
+ r = Redis.new(url: url)
118
+ pong = r.ping
119
+ info = r.info("memory") rescue {}
120
+ used = info["used_memory_human"]
121
+ ["redis", pong || "ok", "memory=#{used}"]
122
+ rescue LoadError
123
+ ["redis", "skipped", "redis gem"]
124
+ rescue StandardError => e
125
+ ["redis", "error", e.message.to_s[0, 120]]
126
+ end
127
+
128
+ def ai_row
129
+ openai = ENV["OPENAI_API_KEY"].present?
130
+ anthropic = ENV["ANTHROPIC_API_KEY"].present?
131
+ detail = "openai=#{openai} anthropic=#{anthropic}"
132
+ ready = defined?(AIEngine) && AIEngine.ready?
133
+ ["ai_engine", ready ? "ready" : "not_ready", detail]
134
+ rescue StandardError => e
135
+ ["ai_engine", "error", e.message.to_s[0, 120]]
136
+ end
137
+
138
+ def queue_summary_row
139
+ return ["solid_queue", "n/a", "tables missing"] unless solid_queue_tables?
140
+
141
+ r = SolidQueue::ReadyExecution.count
142
+ s = SolidQueue::ScheduledExecution.count
143
+ f = SolidQueue::FailedExecution.count
144
+ c = SolidQueue::ClaimedExecution.count
145
+ ["solid_queue", "ok", "ready=#{r} scheduled=#{s} failed=#{f} claimed=#{c}"]
146
+ rescue StandardError => e
147
+ ["solid_queue", "error", e.message.to_s[0, 120]]
148
+ end
149
+
150
+ def queue_rows
151
+ return [] unless solid_queue_tables?
152
+
153
+ SolidQueue::Job.group(:queue_name).count.sort.map do |name, cnt|
154
+ paused = SolidQueue::Queue.find_by_name(name).paused?
155
+ ["queue:#{name}", paused ? "paused" : "active", "jobs=#{cnt}"]
156
+ end
157
+ rescue StandardError => e
158
+ [["queues", "error", e.message.to_s[0, 120]]]
159
+ end
160
+
161
+ def solid_queue_tables?
162
+ ActiveRecord::Base.connection.data_source_exists?("solid_queue_jobs")
163
+ end
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RogIQ
4
+ module Commands
5
+ class Sync < RogIQ::Commands::Base
6
+ desc "postman", "rails postman:sync"
7
+ def postman
8
+ RogIQ.load_rails!
9
+ Rails.application.load_tasks
10
+ Rake::Task["postman:sync"].invoke
11
+ end
12
+
13
+ desc "stripe", "rake stripe:ensure_subscription_prices"
14
+ def stripe
15
+ RogIQ.load_rails!
16
+ Rails.application.load_tasks
17
+ Rake::Task["stripe:ensure_subscription_prices"].invoke
18
+ end
19
+
20
+ desc "wordpress CLIENT", "Enqueue WordPress taxonomies + authors sync jobs"
21
+ def wordpress(identifier)
22
+ RogIQ.load_rails!
23
+ client = RogIQ::Helpers.resolve_client(identifier)
24
+ unless client
25
+ fmt.error_msg("Client not found")
26
+ exit 1
27
+ end
28
+
29
+ ::Integrations::SyncWordpressTaxonomiesJob.perform_later(
30
+ account_id: client.account_id,
31
+ client_id: client.id
32
+ )
33
+ ::Integrations::SyncWordpressAuthorsJob.perform_later(
34
+ account_id: client.account_id,
35
+ client_id: client.id
36
+ )
37
+ fmt.success("WordPress sync jobs enqueued for client #{client.id}.")
38
+ end
39
+
40
+ desc "integrations CLIENT", "Enqueue Integrations::SyncJob for each client integration"
41
+ def integrations(identifier)
42
+ RogIQ.load_rails!
43
+ client = RogIQ::Helpers.resolve_client(identifier)
44
+ unless client
45
+ fmt.error_msg("Client not found")
46
+ exit 1
47
+ end
48
+
49
+ ids = client.integrations.pluck(:id)
50
+ if ids.empty?
51
+ fmt.warn_msg("No integrations linked to this client.")
52
+ return
53
+ end
54
+
55
+ ids.each { |iid| ::Integrations::SyncJob.perform_later(iid) }
56
+ fmt.success("Enqueued #{ids.size} Integrations::SyncJob(s).")
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "fileutils"
5
+
6
+ module RogIQ
7
+ module ConfigStore
8
+ DIR = File.join(Dir.home, ".rogiq").freeze
9
+ PATH = File.join(DIR, "config.yml").freeze
10
+
11
+ module_function
12
+
13
+ def load
14
+ return {} unless File.file?(PATH)
15
+
16
+ YAML.safe_load(File.read(PATH), permitted_classes: [ Symbol, Time ]) || {}
17
+ rescue Psych::SyntaxError
18
+ {}
19
+ end
20
+
21
+ def save(hash)
22
+ FileUtils.mkdir_p(DIR)
23
+ File.write(PATH, YAML.dump(stringify_keys(hash)))
24
+ end
25
+
26
+ def merge!(attrs)
27
+ save(load.merge(stringify_keys(attrs)))
28
+ end
29
+
30
+ def clear!
31
+ FileUtils.rm_f(PATH)
32
+ end
33
+
34
+ def remote_available?
35
+ h = load
36
+ !h["token"].to_s.strip.empty? && !h["api_base_url"].to_s.strip.empty?
37
+ end
38
+
39
+ def api_base_url
40
+ v = load["api_base_url"].to_s.strip
41
+ v.empty? ? nil : v
42
+ end
43
+
44
+ def token
45
+ v = load["token"].to_s.strip
46
+ v.empty? ? nil : v
47
+ end
48
+
49
+ def account_id
50
+ v = load["account_id"]
51
+ s = v.to_s.strip
52
+ s.empty? ? nil : s
53
+ end
54
+
55
+ def stringify_keys(hash)
56
+ hash.to_h.transform_keys(&:to_s)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "date"
5
+
6
+ module RogIQ
7
+ class Formatters
8
+ attr_reader :format, :quiet
9
+
10
+ def initialize(format: "table", quiet: false)
11
+ @format = format.to_s
12
+ @quiet = quiet
13
+ end
14
+
15
+ def say(msg = "")
16
+ Kernel.puts(msg) unless quiet
17
+ end
18
+
19
+ def success(msg)
20
+ say(colorize(msg, "\e[32m"))
21
+ end
22
+
23
+ def warn_msg(msg)
24
+ say(colorize(msg, "\e[33m"))
25
+ end
26
+
27
+ def error_msg(msg)
28
+ $stderr.puts(colorize(msg, "\e[31m"))
29
+ end
30
+
31
+ def output(data)
32
+ if data.is_a?(Hash) && data[:headers] && data[:rows]
33
+ return table(data[:headers], data[:rows]) if format != "json"
34
+
35
+ STDOUT.puts(JSON.pretty_generate({ headers: data[:headers], rows: data[:rows] }))
36
+ return
37
+ end
38
+
39
+ if format == "json"
40
+ STDOUT.puts(JSON.pretty_generate(jsonify(data)))
41
+ return
42
+ end
43
+
44
+ case data
45
+ when Hash
46
+ say(JSON.pretty_generate(jsonify(data)))
47
+ when String
48
+ say(data)
49
+ else
50
+ say(data.inspect)
51
+ end
52
+ end
53
+
54
+ def jsonify(obj)
55
+ case obj
56
+ when Hash
57
+ obj.transform_values { |v| jsonify(v) }
58
+ when Array
59
+ obj.map { |v| jsonify(v) }
60
+ when Time, DateTime
61
+ obj.iso8601
62
+ when Date
63
+ obj.iso8601
64
+ else
65
+ if obj.respond_to?(:iso8601)
66
+ obj.iso8601
67
+ else
68
+ obj
69
+ end
70
+ end
71
+ end
72
+
73
+ def table(headers, rows)
74
+ return if quiet
75
+
76
+ rows = rows.map { |r| r.map(&:to_s) }
77
+ widths = headers.each_with_index.map do |h, i|
78
+ [h.to_s.length, *rows.map { |r| r[i].to_s.length }].max
79
+ end
80
+
81
+ sep = widths.map { |w| "-" * w }.join("-+-")
82
+ header_line = headers.each_with_index.map { |h, i| h.to_s.ljust(widths[i]) }.join(" | ")
83
+ say(header_line)
84
+ say(sep)
85
+ rows.each do |row|
86
+ say(row.each_with_index.map { |cell, i| cell.to_s.ljust(widths[i]) }.join(" | "))
87
+ end
88
+ end
89
+
90
+ def confirm!(prompt, yes: false)
91
+ return true if yes
92
+
93
+ print("#{prompt} [y/N] ")
94
+ $stdin.gets.to_s.strip.downcase.start_with?("y")
95
+ end
96
+
97
+ def colorize(str, code)
98
+ return str unless $stdout.tty?
99
+
100
+ "#{code}#{str}\e[0m"
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RogIQ
4
+ module Helpers
5
+ module_function
6
+
7
+ UUID_RE = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i
8
+
9
+ def uuid?(value)
10
+ value.to_s.match?(UUID_RE)
11
+ end
12
+
13
+ def resolve_client(identifier)
14
+ RogIQ.load_rails!
15
+ s = identifier.to_s.strip
16
+ return nil if s.blank?
17
+
18
+ if uuid?(s)
19
+ ::Client.find_by(id: s)
20
+ else
21
+ ::Client.find_by("LOWER(name) = ?", s.downcase) ||
22
+ ::Client.joins(:account).find_by("LOWER(accounts.slug) = ?", s.downcase) ||
23
+ ::Client.find_by(id: s)
24
+ end
25
+ end
26
+
27
+ def resolve_account(identifier)
28
+ RogIQ.load_rails!
29
+ s = identifier.to_s.strip
30
+ return nil if s.blank?
31
+
32
+ if uuid?(s)
33
+ ::Account.find_by(id: s)
34
+ else
35
+ ::Account.find_by("LOWER(slug) = ?", s.downcase) ||
36
+ ::Account.find_by("LOWER(name) = ?", s.downcase) ||
37
+ ::Account.find_by(id: s)
38
+ end
39
+ end
40
+
41
+ def fmt_from_options(options)
42
+ RogIQ::Formatters.new(format: options[:format] || options["format"], quiet: options[:quiet] || options["quiet"])
43
+ end
44
+
45
+ def with_saved_argv(new_argv)
46
+ old = ARGV.dup
47
+ ARGV.replace(new_argv)
48
+ yield
49
+ ensure
50
+ ARGV.replace(old)
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "uri"
6
+
7
+ module RogIQ
8
+ class HttpApi
9
+ class Error < StandardError
10
+ attr_reader :status, :body
11
+
12
+ def initialize(message, status: nil, body: nil)
13
+ super(message)
14
+ @status = status
15
+ @body = body
16
+ end
17
+ end
18
+
19
+ def self.from_config!
20
+ base = RogIQ::ConfigStore.api_base_url
21
+ token = RogIQ::ConfigStore.token
22
+ raise Error, "Not logged in. Run: rogiq login" if base.nil? || token.nil?
23
+
24
+ new(base: base, token: token, account_id: RogIQ::ConfigStore.account_id)
25
+ end
26
+
27
+ def initialize(base:, token:, account_id: nil)
28
+ @base = base.to_s.chomp("/")
29
+ @token = token
30
+ @account_id = account_id
31
+ end
32
+
33
+ def get(path, query = {})
34
+ request(Net::HTTP::Get, path, query: query)
35
+ end
36
+
37
+ def post(path, body = nil, query: {})
38
+ request(Net::HTTP::Post, path, query: query, body: body)
39
+ end
40
+
41
+ private
42
+
43
+ def request(method_class, path, query: {}, body: nil)
44
+ p = path.start_with?("/") ? path : "/#{path}"
45
+ uri = URI.parse("#{@base}#{p}")
46
+ uri.query = URI.encode_www_form(query.compact) if query.any?
47
+
48
+ req = method_class.new(uri)
49
+ req["Authorization"] = "Bearer #{@token}"
50
+ req["Accept"] = "application/json"
51
+ if body
52
+ req["Content-Type"] = "application/json"
53
+ req.body = JSON.generate(body)
54
+ end
55
+ aid = @account_id.to_s.strip
56
+ req["X-Account-ID"] = aid unless aid.empty?
57
+
58
+ http = Net::HTTP.new(uri.host, uri.port)
59
+ http.use_ssl = uri.scheme == "https"
60
+ http.open_timeout = 15
61
+ http.read_timeout = 120
62
+ res = http.request(req)
63
+ payload = parse_json_body(res)
64
+
65
+ return payload if res.is_a?(Net::HTTPSuccess)
66
+
67
+ msg = [ payload["error"], payload["message"] ].map { |x| x.to_s.strip }.reject(&:empty?).first
68
+ msg ||= "HTTP #{res.code}"
69
+ raise Error.new(msg.to_s, status: res.code.to_i, body: payload)
70
+ end
71
+
72
+ def parse_json_body(res)
73
+ b = res.body.to_s.strip
74
+ return {} if b.empty?
75
+
76
+ JSON.parse(b)
77
+ rescue JSON::ParserError
78
+ { "error" => b[0, 300] }
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RogIQ
4
+ # Boots the Rails application (api/) once. Safe to call multiple times.
5
+ def self.load_rails!
6
+ return if defined?(Rails) && Rails.application&.initialized?
7
+
8
+ root = api_root
9
+ Dir.chdir(root) do
10
+ require File.join(root, "config/environment")
11
+ end
12
+ end
13
+
14
+ def self.api_root
15
+ File.expand_path("../..", __dir__)
16
+ end
17
+
18
+ # Monorepo root (parent of +api/+).
19
+ def self.repo_root
20
+ File.expand_path("../../..", __dir__)
21
+ end
22
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rogiq/remote/command_base"
4
+
5
+ module RogIQ
6
+ module Remote
7
+ class Accounts < RogIQ::Remote::CommandBase
8
+ desc "list", "List accounts"
9
+ method_option :search, type: :string
10
+ method_option :limit, type: :numeric, default: 40, aliases: "-n"
11
+ def list
12
+ q = { limit: options[:limit] }
13
+ q[:search] = options[:search] unless options[:search].to_s.strip.empty?
14
+ emit(api.get("/api/v1/cli/accounts/list", q))
15
+ end
16
+
17
+ desc "show IDENTIFIER", "Account by id, slug, or name"
18
+ def show(identifier)
19
+ emit(api.get("/api/v1/cli/accounts/show", { identifier: identifier }))
20
+ end
21
+
22
+ desc "usage IDENTIFIER", "Usage meters, wallet, open overages"
23
+ def usage(identifier)
24
+ emit(api.get("/api/v1/cli/accounts/usage", { identifier: identifier }))
25
+ end
26
+
27
+ desc "subscription IDENTIFIER", "Subscription + plan summary"
28
+ def subscription(identifier)
29
+ emit(api.get("/api/v1/cli/accounts/subscription", { identifier: identifier }))
30
+ end
31
+
32
+ desc "users IDENTIFIER", "Active account users + roles"
33
+ def users(identifier)
34
+ emit(api.get("/api/v1/cli/accounts/users", { identifier: identifier }))
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rogiq/remote/command_base"
4
+
5
+ module RogIQ
6
+ module Remote
7
+ class Ai < RogIQ::Remote::CommandBase
8
+ desc "status", "AI engine readiness + provider keys"
9
+ def status
10
+ emit(api.get("/api/v1/cli/ai/status"))
11
+ end
12
+
13
+ desc "cost", "Roll up AI usage cost (ai_usage_daily)"
14
+ method_option :days, type: :numeric, default: 30
15
+ method_option :account, type: :string, desc: "Account id or slug"
16
+ def cost
17
+ q = { days: options[:days] }
18
+ q[:account] = options[:account] unless options[:account].to_s.strip.empty?
19
+ emit(api.get("/api/v1/cli/ai/cost", q))
20
+ end
21
+
22
+ desc "models", "List AIModelConfig rows"
23
+ method_option :limit, type: :numeric, default: 100
24
+ def models
25
+ emit(api.get("/api/v1/cli/ai/models", { limit: options[:limit] }))
26
+ end
27
+
28
+ desc "sync", "Enqueue AiModelSyncJob"
29
+ method_option :yes, type: :boolean, default: false, aliases: "-y"
30
+ def sync
31
+ unless options[:yes]
32
+ exit 1 unless yes?("Enqueue AiModelSyncJob?")
33
+ end
34
+
35
+ emit(api.post("/api/v1/cli/ai/sync", { confirm: true }))
36
+ end
37
+
38
+ desc "health_check", "Enqueue AiModelHealthCheckJob"
39
+ map "health-check" => :health_check
40
+ method_option :yes, type: :boolean, default: false, aliases: "-y"
41
+ def health_check
42
+ unless options[:yes]
43
+ exit 1 unless yes?("Enqueue AiModelHealthCheckJob?")
44
+ end
45
+
46
+ emit(api.post("/api/v1/cli/ai/health_check", { confirm: true }))
47
+ end
48
+ end
49
+ end
50
+ end