wiq-cli 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.
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Wiq
6
+ # Walks Thor's command registry and produces a stable JSON shape for agents
7
+ # to introspect the CLI surface in one call.
8
+ #
9
+ # Schema (nested):
10
+ # {
11
+ # "name": "wiq",
12
+ # "version": "0.1.0",
13
+ # "global_options": [ ... ],
14
+ # "top_level_commands": [ ... ],
15
+ # "groups": [ { "name": "...", "description": "...", "commands": [ ... ] } ]
16
+ # }
17
+ module Introspection
18
+ # Thor auto-generates these on every Thor::Group subclass; they're not
19
+ # useful for agents and would just bloat the dump.
20
+ INTERNAL_COMMANDS = %w[help tree].freeze
21
+
22
+ module_function
23
+
24
+ def dump_tree(root: Wiq::CLI)
25
+ {
26
+ "name" => "wiq",
27
+ "version" => Wiq::VERSION,
28
+ "global_options" => dump_options(Wiq::Commands::Base.class_options),
29
+ "top_level_commands" => dump_commands(root.commands, klass: root),
30
+ "groups" => root.subcommand_classes.sort.map do |name, klass|
31
+ dump_group(name, klass, root)
32
+ end
33
+ }
34
+ end
35
+
36
+ def dump_group(name, klass, root)
37
+ {
38
+ "name" => name,
39
+ "description" => root.commands[name]&.description,
40
+ "commands" => dump_commands(klass.commands, klass: klass)
41
+ }
42
+ end
43
+
44
+ def dump_commands(commands, klass:)
45
+ aliases = method_to_alias(klass)
46
+ commands
47
+ .reject { |k, _| INTERNAL_COMMANDS.include?(k) || subcommand_name?(klass, k) }
48
+ .map { |k, cmd| dump_command(cmd, display_name: aliases[k] || k) }
49
+ .sort_by { |row| row["name"] }
50
+ end
51
+
52
+ def dump_command(cmd, display_name:)
53
+ {
54
+ "name" => display_name,
55
+ "description" => cmd.description,
56
+ "long_description" => cmd.long_description,
57
+ "usage" => cmd.usage,
58
+ "options" => dump_options(cmd.options)
59
+ }
60
+ end
61
+
62
+ # Thor's `map` declares user-facing command aliases. `map "run" => :run_report`
63
+ # means when a user types `wiq reports run`, Thor dispatches to method
64
+ # :run_report. Internally Thor stores the command under "run_report" — the
65
+ # method name — but agents need to see "run" (the name they type).
66
+ # Build a reverse map: method_name (String) => alias_name (String).
67
+ #
68
+ # Flag-style aliases (--version, -v) are also registered via `map` for
69
+ # convenience but they're not command names — skip them.
70
+ def method_to_alias(klass)
71
+ return {} unless klass.respond_to?(:map)
72
+
73
+ klass.map.each_with_object({}) do |(alias_name, method_name), h|
74
+ alias_str = alias_name.to_s
75
+ next if alias_str.start_with?("-")
76
+
77
+ h[method_name.to_s] = alias_str
78
+ end
79
+ end
80
+
81
+ def dump_options(options)
82
+ options.sort.map do |name, opt|
83
+ row = {
84
+ "name" => name.to_s,
85
+ "type" => opt.type.to_s,
86
+ "required" => opt.required?,
87
+ "description" => opt.description
88
+ }
89
+ row["default"] = opt.default unless opt.default.nil?
90
+ row["enum"] = opt.enum if opt.enum
91
+ row
92
+ end
93
+ end
94
+
95
+ # When a class registers a `subcommand "foo", FooClass`, Thor also adds
96
+ # a placeholder command named "foo" to the parent's commands hash. Skip
97
+ # those when listing the parent's own commands — they're rendered as
98
+ # groups, not commands.
99
+ def subcommand_name?(klass, command_name)
100
+ return false unless klass.respond_to?(:subcommand_classes)
101
+
102
+ klass.subcommand_classes.key?(command_name)
103
+ end
104
+ end
105
+ end
data/lib/wiq/output.rb ADDED
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Wiq
6
+ # Three output modes:
7
+ # --json full envelope (ok/data/summary/meta) for agents that want structure
8
+ # --agent bare JSON of `data` for headless scripts
9
+ # default pretty JSON to a TTY, or --agent equivalent when piped
10
+ module Output
11
+ module_function
12
+
13
+ def render(data, summary: nil, breadcrumbs: [], meta: {}, options: {})
14
+ mode = pick_mode(options)
15
+ case mode
16
+ when :json
17
+ puts JSON.pretty_generate(envelope(data, summary: summary, breadcrumbs: breadcrumbs, meta: meta))
18
+ when :agent
19
+ puts JSON.generate(data)
20
+ when :pretty
21
+ puts JSON.pretty_generate(data)
22
+ puts "" unless data.nil? || (data.respond_to?(:empty?) && data.empty?)
23
+ puts "→ #{summary}" if summary
24
+ if meta && !meta.empty?
25
+ meta_line = meta.map { |k, v| "#{k}=#{v}" }.join(" ")
26
+ puts " #{meta_line}"
27
+ end
28
+ end
29
+ end
30
+
31
+ def render_error(err, options: {})
32
+ mode = pick_mode(options)
33
+ payload = {
34
+ "ok" => false,
35
+ "error" => err.message,
36
+ "code" => err.respond_to?(:code) ? err.code : "error",
37
+ "hint" => (err.respond_to?(:hint) ? err.hint : nil),
38
+ "details" => (err.respond_to?(:details) ? err.details : nil)
39
+ }.compact
40
+
41
+ case mode
42
+ when :json, :agent
43
+ warn JSON.generate(payload)
44
+ else
45
+ warn "✖ #{payload["error"]} [#{payload["code"]}]"
46
+ warn " hint: #{payload["hint"]}" if payload["hint"]
47
+ end
48
+ end
49
+
50
+ def pick_mode(options)
51
+ options ||= {}
52
+ return :agent if options[:agent] || options["agent"]
53
+ return :json if options[:json] || options["json"]
54
+ $stdout.tty? ? :pretty : :agent
55
+ end
56
+
57
+ def envelope(data, summary:, breadcrumbs:, meta:)
58
+ {
59
+ "ok" => true,
60
+ "data" => data,
61
+ "summary" => summary,
62
+ "breadcrumbs" => breadcrumbs,
63
+ "meta" => meta
64
+ }.compact
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module Wiq
6
+ # Parses the WIQ pagination headers:
7
+ # Link: <https://host/api/v1/foo?page=2>; rel="next", <...>; rel="last"
8
+ # TotalCount: 123
9
+ module Pagination
10
+ module_function
11
+
12
+ # Returns { "next" => "...url...", "prev" => "...", "first" => "...", "last" => "..." }.
13
+ def parse_link(header)
14
+ return {} if header.nil? || header.empty?
15
+
16
+ header.split(",").each_with_object({}) do |part, acc|
17
+ url_part, *params = part.split(";").map(&:strip)
18
+ next unless url_part&.start_with?("<") && url_part.end_with?(">")
19
+
20
+ url = url_part[1..-2]
21
+ rel = params.find { |p| p.start_with?("rel=") }
22
+ next unless rel
23
+
24
+ rel_value = rel.sub(/\Arel="?/, "").sub(/"?\z/, "")
25
+ acc[rel_value] = url
26
+ end
27
+ end
28
+
29
+ def next_url(link_header)
30
+ parse_link(link_header)["next"]
31
+ end
32
+
33
+ def total_count(headers)
34
+ # Header is literally `TotalCount` (case-insensitive via Faraday).
35
+ v = headers["TotalCount"] || headers["totalcount"] || headers["Totalcount"]
36
+ v && Integer(v)
37
+ rescue ArgumentError
38
+ nil
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wiq
4
+ # Translates `--season <year>` into a set of PaidSession ids. No first-class
5
+ # Season exists in WIQ; we approximate by finding paid sessions whose
6
+ # [start_at, end_at] window overlaps the calendar year.
7
+ class SeasonResolver
8
+ def initialize(client)
9
+ @client = client
10
+ end
11
+
12
+ # Returns the array of paid_session hashes that overlap `year`.
13
+ def paid_sessions_for(year)
14
+ year = Integer(year)
15
+ start_of_year = "#{year}-01-01"
16
+ end_of_year = "#{year}-12-31"
17
+
18
+ sessions, = @client.collect_all(
19
+ "/api/v1/paid_sessions",
20
+ {
21
+ "q[start_at_lteq]" => end_of_year,
22
+ "q[end_at_gteq]" => start_of_year,
23
+ "per_page" => 100
24
+ },
25
+ key: "paid_sessions"
26
+ )
27
+ sessions
28
+ end
29
+
30
+ def paid_session_ids_for(year)
31
+ paid_sessions_for(year).map { |ps| ps["id"] }
32
+ end
33
+
34
+ # Filter an array of rosters (as returned by /api/v1/rosters) to those
35
+ # whose roster_syncers point at any paid session id in `ids`.
36
+ def filter_rosters_by_ids(rosters, ids)
37
+ id_set = ids.to_set
38
+ rosters.select do |roster|
39
+ syncers = roster["roster_syncers"] || []
40
+ syncers.any? { |rs| id_set.include?(rs["paid_session_id"]) }
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ require "set"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wiq
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,314 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wiq
4
+ # Named multi-step recipes mapping a human question to a CLI command
5
+ # sequence. Discovery via `wiq workflows list` / `wiq workflows show NAME`.
6
+ #
7
+ # Per-entry schema:
8
+ # name kebab-case slug (the lookup key — also keys the hash)
9
+ # category one of CATEGORIES (used to group `list` output)
10
+ # question natural-language framing — what an agent matches against
11
+ # parameters structured list of placeholders:
12
+ # [{ name:, type:, required:, default?:, enum?:, description?: }]
13
+ # type is one of "date", "integer", "string"
14
+ # recipe ordered list of command strings using two substitution
15
+ # conventions:
16
+ # <name> → required substitution (must match a
17
+ # declared required parameter)
18
+ # [--flag <name>] → optional bracket; drop the whole
19
+ # expression if the parameter is unset
20
+ # admin_only true when the workflow depends on at least one admin-only
21
+ # report or admin-only metric (non-admin coach PATs 403)
22
+ # notes agent-facing guidance (when relevant)
23
+ module Workflows
24
+ CATEGORIES = %w[attendance leads roster finance memberships fundraising store].freeze
25
+
26
+ ALL = {
27
+ # ── Attendance / check-ins ──────────────────────────────────
28
+ "attendance-last-month" => {
29
+ name: "attendance-last-month",
30
+ category: "attendance",
31
+ question: "How many practices did each wrestler attend last month?",
32
+ parameters: [
33
+ { name: "start_date", type: "date", required: true },
34
+ { name: "end_date", type: "date", required: true },
35
+ { name: "roster_id", type: "integer", required: false, default: 0,
36
+ description: "0 = all rosters" }
37
+ ],
38
+ recipe: [
39
+ "wiq reports run CheckInSummaryReport --start <start_date> --end <end_date> [--roster <roster_id>]"
40
+ ],
41
+ notes: "Useful to build a leaderboard of check-ins at your wrestling club."
42
+ },
43
+
44
+ "who-came-today" => {
45
+ name: "who-came-today",
46
+ category: "attendance",
47
+ question: "Who showed up at the club today?",
48
+ parameters: [
49
+ { name: "today_date", type: "date", required: true,
50
+ description: "Today's date in the team's timezone (YYYY-MM-DD)" }
51
+ ],
52
+ recipe: [
53
+ "wiq reports run CheckInFeedReport --start <today_date> --end <today_date>"
54
+ ],
55
+ notes: "The feed includes registration type per check-in, so the agent " \
56
+ "can filter the result client-side to answer follow-ups like " \
57
+ "'who used a trial pass today' or 'who came without an active " \
58
+ "subscription'."
59
+ },
60
+
61
+ "churn-risk" => {
62
+ name: "churn-risk",
63
+ category: "attendance",
64
+ question: "Which subscribers are at risk of canceling because they haven't checked in lately?",
65
+ parameters: [
66
+ { name: "days_threshold", type: "integer", required: true,
67
+ enum: [7, 14, 30, 60, 90],
68
+ description: "Window of inactivity, in days. Any value outside the enum falls back to 30." }
69
+ ],
70
+ recipe: [
71
+ "wiq reports run ChurnRiskReport --roster 0 --days-threshold <days_threshold>"
72
+ ]
73
+ },
74
+
75
+ # ── Leads pipeline (Prospect is the internal name) ──────────
76
+ "overdue-followups" => {
77
+ name: "overdue-followups",
78
+ category: "leads",
79
+ question: "Which lead families have a stale follow-up?",
80
+ parameters: [],
81
+ recipe: [
82
+ "wiq prospect_families list --attention needs_attention --sort oldest_followup"
83
+ ],
84
+ notes: "WIQ calls these 'prospects' internally but 'leads' in the UI — " \
85
+ "both terms refer to the same thing."
86
+ },
87
+
88
+ # ── Roster ops ──────────────────────────────────────────────
89
+ "growth-across-seasons" => {
90
+ name: "growth-across-seasons",
91
+ category: "roster",
92
+ question: "How has roster size changed across seasons?",
93
+ parameters: [
94
+ { name: "year_a", type: "integer", required: true,
95
+ description: "Earlier season year (e.g. 2025)" },
96
+ { name: "year_b", type: "integer", required: true,
97
+ description: "Later season year (e.g. 2026)" }
98
+ ],
99
+ recipe: [
100
+ "wiq paid_sessions list --season <year_a>",
101
+ "wiq rosters list --season <year_a>",
102
+ "wiq paid_sessions list --season <year_b>",
103
+ "wiq rosters list --season <year_b>"
104
+ ],
105
+ notes: "Client-side compare — no first-class growth report exists. " \
106
+ "Clubs are typically seasonal, so the natural comparison is " \
107
+ "season-vs-season (e.g. 2025 season vs 2026 season). Before " \
108
+ "running, clarify whether the user wants a full calendar-year " \
109
+ "diff or a specific roster/registration vs another."
110
+ },
111
+
112
+ "roster-by-zip" => {
113
+ name: "roster-by-zip",
114
+ category: "roster",
115
+ question: "What zip codes does our roster cover?",
116
+ parameters: [
117
+ { name: "address_question_id", type: "integer", required: true,
118
+ description: "RegAddressQuestion id, discoverable via " \
119
+ "`wiq registrations questions --visibility public`" },
120
+ { name: "roster_id", type: "integer", required: false, default: 0,
121
+ description: "0 = all rosters; pass a specific id to scope" }
122
+ ],
123
+ recipe: [
124
+ "wiq registrations questions --visibility public",
125
+ "wiq reports run RosterReport --roster <roster_id> --append-properties <address_question_id>"
126
+ ],
127
+ notes: "Address (with zip) is captured via a registration question, " \
128
+ "not stored on the wrestler directly. RosterReport with " \
129
+ "--append-properties surfaces it as a column. Step 1 of the " \
130
+ "recipe discovers the question id if the agent doesn't know it."
131
+ },
132
+
133
+ # ── Finance (admin-only) ────────────────────────────────────
134
+ "revenue-snapshot" => {
135
+ name: "revenue-snapshot",
136
+ category: "finance",
137
+ question: "How are we doing financially over the last quarter?",
138
+ parameters: [],
139
+ recipe: [
140
+ "wiq metrics show net_volume --range 3m",
141
+ "wiq metrics show mrr --range 3m",
142
+ "wiq metrics show active_subscribers --range 3m"
143
+ ],
144
+ admin_only: true,
145
+ notes: "Three calls because we deliberately didn't ship a `metrics " \
146
+ "dashboard` convenience command — the agent fans out and " \
147
+ "composes the answer. net_volume and mrr return integer cents."
148
+ },
149
+
150
+ "revenue-by-category" => {
151
+ name: "revenue-by-category",
152
+ category: "finance",
153
+ question: "How is revenue broken down across categories (e.g. Level 1 vs Level 2 subscriptions)?",
154
+ parameters: [
155
+ { name: "range", type: "string", required: false, default: "3m",
156
+ enum: ["today", "7d", "4w", "3m", "12m", "year to date", "custom"],
157
+ description: "Time window for the breakdown." }
158
+ ],
159
+ recipe: [
160
+ "wiq metrics show category_breakdown_net --range <range>"
161
+ ],
162
+ admin_only: true,
163
+ notes: "Returns revenue grouped by category → subcategory → " \
164
+ "detail_category. Common asks: comparing subscription tiers " \
165
+ "(Level 1 vs Level 2), comparing registration revenue vs " \
166
+ "donation revenue, or seeing which revenue source is growing. " \
167
+ "Values are integer cents."
168
+ },
169
+
170
+ "paid-session-accounting" => {
171
+ name: "paid-session-accounting",
172
+ category: "finance",
173
+ question: "Give me a line-item breakdown for this registration session.",
174
+ parameters: [
175
+ { name: "paid_session_id", type: "integer", required: true,
176
+ description: "Discoverable via `wiq paid_sessions list`" }
177
+ ],
178
+ recipe: [
179
+ "wiq reports run PaidSessionAccountingReport --paid-session <paid_session_id>"
180
+ ],
181
+ admin_only: true
182
+ },
183
+
184
+ "failed-payments-recent" => {
185
+ name: "failed-payments-recent",
186
+ category: "finance",
187
+ question: "Which families have outstanding failed payments (after filtering out failures that were later retried successfully)?",
188
+ parameters: [
189
+ { name: "since_date", type: "date", required: true,
190
+ description: "Earliest created_at to scan from (YYYY-MM-DD)" }
191
+ ],
192
+ recipe: [
193
+ "wiq charges list --status failed --since <since_date> --all"
194
+ ],
195
+ admin_only: true,
196
+ notes: "CRITICAL: a failed charge alone is NOT actionable. " \
197
+ "Stripe/Justifi retry subscriptions automatically and " \
198
+ "customers retry registrations after card declines, so most " \
199
+ "failures resolve themselves. The cross-check is mandatory:\n\n" \
200
+ "After the step-1 list returns, for EACH failed charge:\n" \
201
+ " 1. Note its billing_profile_id, chargeable_id, " \
202
+ "chargeable_type, and created_at.\n" \
203
+ " 2. Run `wiq charges list --billing-profile <id> " \
204
+ "--status successful --since <failure_created_at> --all`.\n" \
205
+ " 3. Drop the failed charge if any returned successful " \
206
+ "charge has matching chargeable_id AND chargeable_type.\n\n" \
207
+ "What remains is the actionable set: failures with no later " \
208
+ "successful retry on the same item. Surface those to the " \
209
+ "user with family name + amount + chargeable description."
210
+ },
211
+
212
+ "family-payment-history" => {
213
+ name: "family-payment-history",
214
+ category: "finance",
215
+ question: "What has this family paid (or tried to pay) recently?",
216
+ parameters: [
217
+ { name: "billing_profile_id", type: "integer", required: true,
218
+ description: "Discover via `wiq billing_profiles show <profile_id> --profile-type ParentProfile`" },
219
+ { name: "since_date", type: "date", required: false, default: "90 days ago",
220
+ description: "Earliest created_at; default is roughly 90 days back when omitted client-side" }
221
+ ],
222
+ recipe: [
223
+ "wiq charges list --billing-profile <billing_profile_id> [--since <since_date>] --all"
224
+ ],
225
+ admin_only: true,
226
+ notes: "Returns the family's charge history — both successful and " \
227
+ "failed attempts, with the chargeable description (what was " \
228
+ "paid for) on each row. To go from a wrestler name to a " \
229
+ "billing_profile_id: `wiq wrestlers list --query \"Name\"`, " \
230
+ "then `wiq wrestlers show <id>` to find the parent profile " \
231
+ "id, then `wiq billing_profiles show <parent_id> " \
232
+ "--profile-type ParentProfile`."
233
+ },
234
+
235
+ "scholarship-audit" => {
236
+ name: "scholarship-audit",
237
+ category: "finance",
238
+ question: "Where is scholarship money going?",
239
+ parameters: [
240
+ { name: "start_date", type: "date", required: true },
241
+ { name: "end_date", type: "date", required: true }
242
+ ],
243
+ recipe: [
244
+ "wiq reports run ScholarshipAuditReport --start <start_date> --end <end_date>"
245
+ ],
246
+ admin_only: true,
247
+ notes: "Also requires team.payments_enabled — the UI hides the tab " \
248
+ "otherwise, but the API just returns empty data."
249
+ },
250
+
251
+ # ── Memberships (USAW / AAU) ────────────────────────────────
252
+ "usaw-renewal-batch" => {
253
+ name: "usaw-renewal-batch",
254
+ category: "memberships",
255
+ question: "Which wrestlers need their USAW renewed before our next event?",
256
+ parameters: [
257
+ { name: "roster_id", type: "integer", required: false, default: 0,
258
+ description: "0 = all rosters" }
259
+ ],
260
+ recipe: [
261
+ "wiq reports run UsawExpiredReport [--roster <roster_id>]"
262
+ ],
263
+ notes: "Output is a bulk-purchase upload format usable directly in " \
264
+ "USAW's system."
265
+ },
266
+
267
+ "aau-renewal-batch" => {
268
+ name: "aau-renewal-batch",
269
+ category: "memberships",
270
+ question: "Which wrestlers need their AAU memberships renewed?",
271
+ parameters: [
272
+ { name: "roster_id", type: "integer", required: false, default: 0,
273
+ description: "0 = all rosters" }
274
+ ],
275
+ recipe: [
276
+ "wiq reports run AauExpiredReport [--roster <roster_id>]"
277
+ ],
278
+ notes: "Output is a bulk-purchase upload format usable directly in " \
279
+ "AAU's system."
280
+ },
281
+
282
+ # ── Fundraising ─────────────────────────────────────────────
283
+ "fundraiser-status" => {
284
+ name: "fundraiser-status",
285
+ category: "fundraising",
286
+ question: "How is our active fundraiser doing?",
287
+ parameters: [
288
+ { name: "fundraiser_id", type: "integer", required: true,
289
+ description: "Discoverable via `wiq fundraisers list` (not yet on the CLI surface — " \
290
+ "open the WIQ web UI for now)" }
291
+ ],
292
+ recipe: [
293
+ "wiq reports run FundraiserSummaryReport --fundraiser <fundraiser_id>"
294
+ ],
295
+ admin_only: true
296
+ },
297
+
298
+ # ── Online store ────────────────────────────────────────────
299
+ "store-orders" => {
300
+ name: "store-orders",
301
+ category: "store",
302
+ question: "What's been sold in our online store?",
303
+ parameters: [
304
+ { name: "online_store_id", type: "integer", required: true,
305
+ description: "Discoverable via the WIQ web UI (not yet on the CLI surface)" }
306
+ ],
307
+ recipe: [
308
+ "wiq reports run OnlineStoreSummaryReport --online-store <online_store_id>"
309
+ ],
310
+ admin_only: true
311
+ }
312
+ }.freeze
313
+ end
314
+ end
data/lib/wiq.rb ADDED
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "wiq/version"
4
+ require "wiq/errors"
5
+ require "wiq/credentials"
6
+ require "wiq/config"
7
+ require "wiq/output"
8
+ require "wiq/pagination"
9
+ require "wiq/client"
10
+ require "wiq/season_resolver"
11
+ require "wiq/introspection"
12
+ require "wiq/workflows"
13
+ require "wiq/commands/base"
14
+ require "wiq/commands/auth"
15
+ require "wiq/commands/doctor"
16
+ require "wiq/commands/check_ins"
17
+ require "wiq/commands/reports"
18
+ require "wiq/commands/paid_sessions"
19
+ require "wiq/commands/registrations"
20
+ require "wiq/commands/metrics"
21
+ require "wiq/commands/events"
22
+ require "wiq/commands/rosters"
23
+ require "wiq/commands/prospects"
24
+ require "wiq/commands/prospect_families"
25
+ require "wiq/commands/workflows"
26
+ require "wiq/commands/setup"
27
+ require "wiq/commands/wrestlers"
28
+ require "wiq/commands/charges"
29
+ require "wiq/commands/billing_profiles"
30
+ require "wiq/cli"