@garuhq/cli 0.5.0 → 0.7.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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,67 @@
3
3
  All notable changes to `@garuhq/cli` are documented in this file. Format:
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning: [SemVer](https://semver.org/).
5
5
 
6
+ ## [0.7.0] — 2026-05-31
7
+
8
+ ### Added
9
+
10
+ - **Pix Automático** (BACEN auto-debit recurring Pix) across the CLI.
11
+ - `garu scheduled-charges create --methods pix_automatic` — schedule a
12
+ Pix Automático recurring series. The CLI validates locally that
13
+ `pix_automatic` is paired with `--type=recurring` **and** a
14
+ `--product-id`, returning an `invalid_input` error before any network
15
+ round-trip if either is missing.
16
+ - `garu products` — a new command group wrapping the SDK's product
17
+ write surface:
18
+ - `garu products create --name <name> [...]` — create a product.
19
+ - `garu products update <id> [...]` — partial update by numeric id or
20
+ UUID; only the flags you pass are changed.
21
+ - Both accept `--pix-automatic` / `--no-pix-automatic` to toggle Pix
22
+ Automático on the product's subscription checkout, alongside
23
+ `--pix`, `--boleto`, `--credit-card`, `--value` (centavos),
24
+ `--installments`, `--subscription`, and related fields.
25
+ - `garu --help` now includes an end-to-end **Pix Automático recurring
26
+ subscription** recipe (create the product, then schedule the
27
+ recurring charge).
28
+
29
+ ### Changed
30
+
31
+ - `@garuhq/node` SDK bumped to 0.15.0 for `pix_automatic` on scheduled
32
+ charges, the `pixAutomatic` product field, and the new
33
+ `products.create()` / `products.update()` methods.
34
+
35
+ ## [0.6.0] — 2026-05-25
36
+
37
+ ### Added
38
+
39
+ - `garu scheduled-charges` — a new command group for future-dated
40
+ charges, wrapping the full `@garuhq/node` `scheduledCharges` surface:
41
+ `create`, `list`, `get`, `postpone`, `pause`, `resume`, `mark-paid`,
42
+ `cancel-recurrence`, `cancel-at-period-end`, `change-payment-method`,
43
+ `clear-payment-method`, and `attempts`.
44
+ - `garu scheduled-charges charge-now <id>` — dispatch a scheduled
45
+ charge immediately (the same charge + notification the daily cron
46
+ sends on the due date), instead of waiting for `dueDate`.
47
+ - **Idempotent**: a cycle whose d-day was already dispatched reports
48
+ `already_sent` and is **not** re-charged.
49
+ - The returned pt-BR `message` is always printed; in `--json` mode
50
+ the full `{ outcome, cycleNumber, reason?, message }` is emitted on
51
+ stdout.
52
+ - **Exits non-zero** on outcome `failed`/`not_sent` and on a 4xx
53
+ rejection (`400` for a non-billable status or a recurring series
54
+ with no open cycle; `404` if the charge isn't yours), so it
55
+ composes in shell pipelines. `dispatched` and `already_sent`
56
+ exit `0`.
57
+ - `garu scheduled-charges create --max-recovery-days <n>` — set how
58
+ many days past `dueDate` the daily recovery sweep keeps auto-billing
59
+ a missed charge (integer 1–365). Omit for the system default (14).
60
+
61
+ ### Changed
62
+
63
+ - `@garuhq/node` SDK bumped to 0.13.0 for the new
64
+ `scheduledCharges.chargeNow()` method and the `maxRecoveryDays`
65
+ field on create params / `ScheduledChargeRecord`.
66
+
6
67
  ## [0.5.0] — 2026-05-19
7
68
 
8
69
  ### Added
@@ -14,14 +75,14 @@ All notable changes to `@garuhq/cli` are documented in this file. Format:
14
75
  then dispatches that clone. The original failure record (response
15
76
  status, response body, attempts, timestamps) stays intact.
16
77
  - In pretty mode the CLI prints `✓ Resent event <src> → new event
17
- <clone>` to stderr so the new id is impossible to miss; the
78
+ <clone>` to stderr so the new id is impossible to miss; the
18
79
  cloned event itself is rendered on stdout with a new `resendOf:`
19
80
  line.
20
81
  - In JSON mode the cloned event is the full stdout payload —
21
82
  `.id` is the new event, `.manualResendOf` is the source.
22
83
  - Recipient handlers will see this as a distinct delivery: the
23
84
  gateway POSTs the clone with `Idempotency-Key:
24
- resend_<originalId>`.
85
+ resend_<originalId>`.
25
86
 
26
87
  ### Deprecated
27
88
 
@@ -50,7 +111,7 @@ All notable changes to `@garuhq/cli` are documented in this file. Format:
50
111
  - **`garu --version`** correctly reports `0.4.2`. The `0.4.1` release
51
112
  shipped with `src/version.ts:CLI_VERSION` still pinned to `'0.4.0'`
52
113
  because that file was missed during the bump. A new `scripts/
53
- check-version-sync.mjs` runs as part of `prepublishOnly` and fails
114
+ check-version-sync.mjs` runs as part of `prepublishOnly` and fails
54
115
  the build if `package.json:version` and `src/version.ts:CLI_VERSION`
55
116
  drift apart.
56
117
 
@@ -71,7 +132,7 @@ All notable changes to `@garuhq/cli` are documented in this file. Format:
71
132
  - Bump `@garuhq/node` to `0.11.1` to pick up the empty-body POST fix.
72
133
  `garu webhooks events retry <id>` and `garu scheduled-charges resume <id>`
73
134
  were failing against production with `Body cannot be empty when content-type
74
- is set to 'application/json'`. The SDK now sends an explicit `{}` body on
135
+ is set to 'application/json'`. The SDK now sends an explicit `{}` body on
75
136
  every otherwise-empty mutation.
76
137
 
77
138
  ## [0.4.0] — 2026-05-19
@@ -81,15 +142,15 @@ All notable changes to `@garuhq/cli` are documented in this file. Format:
81
142
  - `garu webhooks events` command tree — inspect and replay webhook
82
143
  deliveries from the CLI. The dashboard "Reenviar" button is now
83
144
  available from the API key auth path as well, which means support
84
- + on-call workflows can resend events from a terminal instead of
85
- having to log into the dashboard.
86
- - `garu webhooks events list [--status <s>] [--event-type <t>] [--endpoint-id <n>] [--page <n>] [--limit <n>]`
145
+ - on-call workflows can resend events from a terminal instead of
146
+ having to log into the dashboard.
147
+ * `garu webhooks events list [--status <s>] [--event-type <t>] [--endpoint-id <n>] [--page <n>] [--limit <n>]`
87
148
  — paginated listing with status badges (green `success`, yellow
88
149
  `pending`, red `failed`) in TTY mode.
89
- - `garu webhooks events get <id>` — fetch one webhook event with
150
+ * `garu webhooks events get <id>` — fetch one webhook event with
90
151
  the full endpoint snapshot, response status, and (truncated)
91
152
  response body.
92
- - `garu webhooks events retry <id>` — re-deliver a webhook event
153
+ * `garu webhooks events retry <id>` — re-deliver a webhook event
93
154
  (resets to `pending` and triggers an immediate attempt). Works on
94
155
  any status; use this when a customer reports a missed event.
95
156
 
package/README.md CHANGED
@@ -59,11 +59,11 @@ garu doctor
59
59
 
60
60
  The CLI resolves your API key using the following priority chain:
61
61
 
62
- | Priority | Source | How to set |
63
- | ----------- | ------------------------ | --------------------------------------- |
64
- | 1 (highest) | `--api-key` flag | `garu --api-key sk_live_... charges list` |
65
- | 2 | `GARU_API_KEY` env var | `export GARU_API_KEY=sk_live_...` |
66
- | 3 (lowest) | Config file | `garu login` |
62
+ | Priority | Source | How to set |
63
+ | ----------- | ---------------------- | ----------------------------------------- |
64
+ | 1 (highest) | `--api-key` flag | `garu --api-key sk_live_... charges list` |
65
+ | 2 | `GARU_API_KEY` env var | `export GARU_API_KEY=sk_live_...` |
66
+ | 3 (lowest) | Config file | `garu login` |
67
67
 
68
68
  If no key is found from any source, the CLI errors with code `auth_error`.
69
69
 
@@ -112,9 +112,9 @@ garu login
112
112
  garu login --api-key sk_live_... --profile production
113
113
  ```
114
114
 
115
- | Flag | Description |
116
- | ---------------------- | ---------------------------------------- |
117
- | `--api-key <key>` | Pre-supply the key instead of prompting |
115
+ | Flag | Description |
116
+ | ---------------------- | ------------------------------------------------ |
117
+ | `--api-key <key>` | Pre-supply the key instead of prompting |
118
118
  | `-p, --profile <name>` | Profile name to store under (default: `default`) |
119
119
 
120
120
  On success, credentials are saved to `~/.config/garu/credentials.json` with `0600` permissions (owner read/write only).
@@ -130,9 +130,9 @@ garu logout
130
130
  garu logout --profile test
131
131
  ```
132
132
 
133
- | Flag | Description |
134
- | ---------------------- | ---------------------------------------------------- |
135
- | `-p, --profile <name>` | Only remove this profile instead of the whole file |
133
+ | Flag | Description |
134
+ | ---------------------- | -------------------------------------------------- |
135
+ | `-p, --profile <name>` | Only remove this profile instead of the whole file |
136
136
 
137
137
  ---
138
138
 
@@ -156,13 +156,13 @@ garu charges list --status paid --limit 50
156
156
  garu charges list --search "Maria" --payment-method pix
157
157
  ```
158
158
 
159
- | Flag | Description |
160
- | --------------------------- | ---------------------------------------------- |
161
- | `--page <n>` | Page number (1-based) |
162
- | `--limit <n>` | Items per page (1-100) |
163
- | `--status <status>` | Filter by status (e.g. `paid`, `pending`) |
164
- | `--search <query>` | Search by customer name, email, or document |
165
- | `--payment-method <method>` | Filter: `pix`, `creditcard`, `boleto` |
159
+ | Flag | Description |
160
+ | --------------------------- | ------------------------------------------- |
161
+ | `--page <n>` | Page number (1-based) |
162
+ | `--limit <n>` | Items per page (1-100) |
163
+ | `--status <status>` | Filter by status (e.g. `paid`, `pending`) |
164
+ | `--search <query>` | Search by customer name, email, or document |
165
+ | `--payment-method <method>` | Filter: `pix`, `creditcard`, `boleto` |
166
166
 
167
167
  ---
168
168
 
@@ -191,21 +191,21 @@ garu charges create --type credit_card --product-id prod-uuid \
191
191
  --installments 3
192
192
  ```
193
193
 
194
- | Flag | Required | Description |
195
- | -------------------------------- | -------- | ------------------------------------------ |
196
- | `--type <type>` | Yes | Payment method: `pix`, `credit_card`, `boleto` |
197
- | `--product-id <uuid>` | Yes | Product UUID |
198
- | `--customer-name <name>` | Yes | Customer full name |
199
- | `--customer-email <email>` | Yes | Customer email |
200
- | `--customer-document <doc>` | Yes | CPF (11 digits) or CNPJ (14 digits) |
201
- | `--customer-phone <phone>` | Yes | Phone with area code, digits only |
202
- | `--card-number <number>` | credit_card | Credit card number |
203
- | `--card-cvv <cvv>` | credit_card | Credit card CVV |
204
- | `--card-expiration <yyyy-mm>` | credit_card | Expiration date |
205
- | `--card-holder <name>` | credit_card | Cardholder name |
206
- | `--installments <n>` | No | Number of installments, 1-12 (default: 1) |
207
- | `--additional-info <text>` | No | Free-form metadata |
208
- | `--idempotency-key <key>` | No | Idempotency key (auto-generated if omitted) |
194
+ | Flag | Required | Description |
195
+ | ----------------------------- | ----------- | ---------------------------------------------- |
196
+ | `--type <type>` | Yes | Payment method: `pix`, `credit_card`, `boleto` |
197
+ | `--product-id <uuid>` | Yes | Product UUID |
198
+ | `--customer-name <name>` | Yes | Customer full name |
199
+ | `--customer-email <email>` | Yes | Customer email |
200
+ | `--customer-document <doc>` | Yes | CPF (11 digits) or CNPJ (14 digits) |
201
+ | `--customer-phone <phone>` | Yes | Phone with area code, digits only |
202
+ | `--card-number <number>` | credit_card | Credit card number |
203
+ | `--card-cvv <cvv>` | credit_card | Credit card CVV |
204
+ | `--card-expiration <yyyy-mm>` | credit_card | Expiration date |
205
+ | `--card-holder <name>` | credit_card | Cardholder name |
206
+ | `--installments <n>` | No | Number of installments, 1-12 (default: 1) |
207
+ | `--additional-info <text>` | No | Free-form metadata |
208
+ | `--idempotency-key <key>` | No | Idempotency key (auto-generated if omitted) |
209
209
 
210
210
  ---
211
211
 
@@ -232,11 +232,99 @@ garu charges refund 4472
232
232
  garu charges refund 4472 --amount 1000 --reason "customer_request"
233
233
  ```
234
234
 
235
- | Flag | Description |
236
- | ------------------------- | --------------------------------------------- |
235
+ | Flag | Description |
236
+ | ------------------------- | ------------------------------------------------- |
237
237
  | `--amount <centavos>` | Partial refund amount in centavos (omit for full) |
238
- | `--reason <text>` | Optional refund reason |
239
- | `--idempotency-key <key>` | Idempotency key (auto-generated if omitted) |
238
+ | `--reason <text>` | Optional refund reason |
239
+ | `--idempotency-key <key>` | Idempotency key (auto-generated if omitted) |
240
+
241
+ ---
242
+
243
+ ### `garu scheduled-charges create`
244
+
245
+ Schedule a future-dated charge — one-time or recurring (PIX, Boleto, or Card).
246
+
247
+ ```bash
248
+ # One-time PIX/Boleto charge
249
+ garu scheduled-charges create \
250
+ --customer-id 42 --amount 297.50 --type one_time \
251
+ --due-date 2026-06-15 --methods pix,boleto \
252
+ --description "Mensalidade Junho"
253
+
254
+ # Recurring card subscription, custom recovery window
255
+ garu scheduled-charges create \
256
+ --customer-id 42 --amount 99.00 --type recurring \
257
+ --due-date 2026-06-15 --methods card --product-id 5 \
258
+ --recurrence-interval monthly --recurrence-ends-after 12 \
259
+ --max-recovery-days 30
260
+ ```
261
+
262
+ | Flag | Description |
263
+ | --------------------------------- | ------------------------------------------------------------------------- |
264
+ | `--customer-id <n>` | Customer id (required) |
265
+ | `--amount <brl>` | Decimal BRL amount, e.g. `297.50` (required) |
266
+ | `--type <type>` | `one_time` or `recurring` (required) |
267
+ | `--due-date <yyyy-mm-dd>` | First due date in São Paulo time (required) |
268
+ | `--methods <list>` | Comma-separated: `pix,boleto,card` (required; `card` is recurring-only) |
269
+ | `--product-id <n>` | Product id (required when `methods` includes `card`) |
270
+ | `--description <text>` | Charge description |
271
+ | `--recurrence-interval <i>` | `weekly`/`biweekly`/`monthly`/`bimonthly`/`quarterly`/`biannual`/`yearly` |
272
+ | `--recurrence-interval-count <n>` | Multiplier for the interval |
273
+ | `--recurrence-ends-after <n>` | Stop after N successful cycles |
274
+ | `--recurrence-ends-on <date>` | Stop after this calendar date |
275
+ | `--trial-days <n>` | Free-trial days (1–365, recurring-only) |
276
+ | `--external-reference <ref>` | Your own reconciliation reference |
277
+ | `--metadata <json>` | JSON object of custom metadata |
278
+ | `--max-recovery-days <n>` | Days past due the recovery sweep keeps auto-billing (1–365; default 14) |
279
+ | `--idempotency-key <key>` | Idempotency key (auto-generated if omitted) |
280
+
281
+ ---
282
+
283
+ ### `garu scheduled-charges charge-now`
284
+
285
+ Dispatch a scheduled charge **now** — the same charge + notification the daily
286
+ cron would send on the due date — instead of waiting for `dueDate`.
287
+
288
+ ```bash
289
+ garu scheduled-charges charge-now sch_abc123
290
+
291
+ # Compose in scripts: non-zero exit on a negative outcome
292
+ if garu scheduled-charges charge-now sch_abc123 --json | jq -e '.outcome=="dispatched"'; then
293
+ echo "sent"
294
+ fi
295
+ ```
296
+
297
+ **Idempotent.** If this cycle's d-day was already dispatched, the command
298
+ reports `already_sent` and does **not** re-charge. The returned `message`
299
+ (pt-BR, ready to show) is always printed; in `--json` mode the full result —
300
+ `{ outcome, cycleNumber, reason?, message }` — is written to stdout.
301
+
302
+ The process **exits non-zero** when:
303
+
304
+ - `outcome` is `failed` (card charge failed — see `reason`, e.g. `card_expired`) or
305
+ `not_sent` (couldn't dispatch — e.g. `no_email`, `no_saved_payment_method`), and
306
+ - the gateway rejects the request with a 4xx — `400` if the charge isn't in a
307
+ billable status (`scheduled`/`due_today`) or a recurring series has no open
308
+ cycle, `404` if the charge isn't yours.
309
+
310
+ `dispatched` and `already_sent` exit `0`.
311
+
312
+ ---
313
+
314
+ ### `garu scheduled-charges` — other subcommands
315
+
316
+ | Command | Description |
317
+ | ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
318
+ | `list [--page --limit --customer-id --status --type ...]` | List scheduled charges (`--status` repeatable; `--due-from/--due-to`, `--search`) |
319
+ | `get <id>` | Fetch a charge with its event timeline and linked transactions |
320
+ | `postpone <id> --new-due-date <date> [--reason]` | Move to a new due date |
321
+ | `pause <id> [--reason]` / `resume <id>` | Pause (no reminders fire) / resume |
322
+ | `mark-paid <id> --payment-date <date> [--external-reference --cycle-number]` | Mark paid out-of-band (`--cycle-number` required for recurring) |
323
+ | `cancel-recurrence <id> [--reason]` | Stop future cycles of a recurring series |
324
+ | `cancel-at-period-end <id> [--disable]` | Toggle Stripe-style soft cancel (omit `--disable` to enable) |
325
+ | `change-payment-method <id> --payment-method-id <n>` | Swap the saved card on a recurring series |
326
+ | `clear-payment-method <id>` | Clear the saved card (future cycles fall back to email-with-link) |
327
+ | `attempts <id> [--page --limit --cycle-number]` | Per-attempt billing log |
240
328
 
241
329
  ---
242
330
 
@@ -274,10 +362,10 @@ These flags work on every command:
274
362
  garu [global options] <command> [command options]
275
363
  ```
276
364
 
277
- | Flag | Description |
278
- | ---------------------- | ------------------------------------------------------------ |
365
+ | Flag | Description |
366
+ | ---------------------- | ------------------------------------------------------------- |
279
367
  | `--api-key <key>` | Override API key for this invocation (takes highest priority) |
280
- | `-p, --profile <name>` | Credentials profile to use |
368
+ | `-p, --profile <name>` | Credentials profile to use |
281
369
  | `--json` | Force JSON output even in interactive terminals |
282
370
  | `-q, --quiet` | Suppress status output; only print results and errors |
283
371
  | `-v, --version` | Print version and exit |
@@ -289,10 +377,10 @@ garu [global options] <command> [command options]
289
377
 
290
378
  The CLI has two output modes:
291
379
 
292
- | Mode | When | Stdout | Stderr |
293
- | --------------- | ---------------------- | -------------- | ---------------- |
294
- | **Interactive** | Terminal (TTY) | Formatted text | Status lines |
295
- | **Machine** | Piped, CI, or `--json` | JSON | Nothing |
380
+ | Mode | When | Stdout | Stderr |
381
+ | --------------- | ---------------------- | -------------- | ------------ |
382
+ | **Interactive** | Terminal (TTY) | Formatted text | Status lines |
383
+ | **Machine** | Piped, CI, or `--json` | JSON | Nothing |
296
384
 
297
385
  Switching is automatic -- pipe to another command and JSON output activates:
298
386
 
@@ -338,11 +426,11 @@ Agents calling the CLI as a subprocess automatically get JSON output (non-TTY de
338
426
 
339
427
  ## Configuration
340
428
 
341
- | Item | Path | Notes |
342
- | ----------------- | --------------------------------- | ----------------------------------------- |
343
- | Config directory | `~/.config/garu/` | Respects `$XDG_CONFIG_HOME` |
344
- | Credentials | `~/.config/garu/credentials.json` | `0600` permissions (owner read/write) |
345
- | Override path | `$GARU_CREDENTIALS_PATH` | Point to a custom credentials file |
429
+ | Item | Path | Notes |
430
+ | ---------------- | --------------------------------- | ------------------------------------- |
431
+ | Config directory | `~/.config/garu/` | Respects `$XDG_CONFIG_HOME` |
432
+ | Credentials | `~/.config/garu/credentials.json` | `0600` permissions (owner read/write) |
433
+ | Override path | `$GARU_CREDENTIALS_PATH` | Point to a custom credentials file |
346
434
 
347
435
  ---
348
436
 
package/dist/index.cjs CHANGED
@@ -15,7 +15,7 @@ var pc__default = /*#__PURE__*/_interopDefault(pc);
15
15
  // src/index.ts
16
16
 
17
17
  // src/version.ts
18
- var CLI_VERSION = "0.5.0";
18
+ var CLI_VERSION = "0.7.0";
19
19
  var CliError = class extends Error {
20
20
  code;
21
21
  exitCode;
@@ -475,6 +475,8 @@ async function logoutCommand(opts = {}) {
475
475
  printSuccess(`Profile '${opts.profile}' removed.`, opts);
476
476
  return { cleared: opts.profile };
477
477
  }
478
+
479
+ // src/commands/products.ts
478
480
  async function getClient2(opts) {
479
481
  if (opts.garu) return opts.garu;
480
482
  const auth = await resolveAuth({
@@ -486,8 +488,292 @@ async function getClient2(opts) {
486
488
  ...opts.baseUrl !== void 0 ? { baseUrl: opts.baseUrl } : {}
487
489
  });
488
490
  }
489
- async function webhooksEventsListCommand(opts) {
491
+ function buildProductBody(opts) {
492
+ const body = {};
493
+ if (opts.name !== void 0) body.name = opts.name;
494
+ if (opts.value !== void 0) body.value = opts.value;
495
+ if (opts.description !== void 0) body.description = opts.description;
496
+ if (opts.image !== void 0) body.image = opts.image;
497
+ if (opts.tags !== void 0) body.tags = opts.tags;
498
+ if (opts.pix !== void 0) body.pix = opts.pix;
499
+ if (opts.boleto !== void 0) body.boleto = opts.boleto;
500
+ if (opts.creditCard !== void 0) body.creditCard = opts.creditCard;
501
+ if (opts.pixAutomatic !== void 0) body.pixAutomatic = opts.pixAutomatic;
502
+ if (opts.installments !== void 0) body.installments = opts.installments;
503
+ if (opts.isSubscription !== void 0) body.isSubscription = opts.isSubscription;
504
+ if (opts.subscriptionType !== void 0) body.subscriptionType = opts.subscriptionType;
505
+ if (opts.unitLabel !== void 0) body.unitLabel = opts.unitLabel;
506
+ if (opts.returnUrl !== void 0) body.returnUrl = opts.returnUrl;
507
+ if (opts.returnUrlButtonText !== void 0) body.returnUrlButtonText = opts.returnUrlButtonText;
508
+ return body;
509
+ }
510
+ async function productsCreateCommand(opts) {
511
+ const garu = await getClient2(opts);
512
+ const params = { ...buildProductBody(opts), name: opts.name };
513
+ const product = await garu.products.create(params);
514
+ printSuccess(`Created product ${product.uuid ?? product.id}`, opts);
515
+ printResult(product, { ...opts, prettyPrint: prettyProduct });
516
+ return product;
517
+ }
518
+ async function productsUpdateCommand(opts) {
519
+ const body = buildProductBody(opts);
520
+ if (Object.keys(body).length === 0) {
521
+ throw new CliError("invalid_input", "Nothing to update \u2014 pass at least one field to change.");
522
+ }
490
523
  const garu = await getClient2(opts);
524
+ const product = await garu.products.update(opts.id, body);
525
+ printSuccess(`Updated product ${product.uuid ?? product.id}`, opts);
526
+ printResult(product, { ...opts, prettyPrint: prettyProduct });
527
+ return product;
528
+ }
529
+ function prettyProduct(p) {
530
+ const methods = [
531
+ p.pix ? "pix" : null,
532
+ p.boleto ? "boleto" : null,
533
+ p.creditCard ? "card" : null,
534
+ p.pixAutomatic ? "pix_automatic" : null
535
+ ].filter(Boolean).join(", ");
536
+ const lines = [
537
+ `Product ${p.uuid ?? p.id}`,
538
+ ` name: ${p.name}`,
539
+ ` value: ${p.value} (centavos)`,
540
+ ` methods: ${methods || "(none)"}`
541
+ ];
542
+ if (p.description) lines.push(` description: ${p.description}`);
543
+ return lines.join("\n");
544
+ }
545
+
546
+ // src/commands/scheduled-charges.ts
547
+ async function getClient3(opts) {
548
+ if (opts.garu) return opts.garu;
549
+ const auth = await resolveAuth({
550
+ ...opts.apiKey !== void 0 ? { apiKey: opts.apiKey } : {},
551
+ ...opts.profile !== void 0 ? { profile: opts.profile } : {}
552
+ });
553
+ return createGaruClient({
554
+ auth,
555
+ ...opts.baseUrl !== void 0 ? { baseUrl: opts.baseUrl } : {}
556
+ });
557
+ }
558
+ function buildRecurrence(opts) {
559
+ if (opts.recurrenceInterval === void 0) return void 0;
560
+ const recurrence = { interval: opts.recurrenceInterval };
561
+ if (opts.recurrenceIntervalCount !== void 0)
562
+ recurrence.intervalCount = opts.recurrenceIntervalCount;
563
+ if (opts.recurrenceEndsAfter !== void 0) recurrence.endsAfter = opts.recurrenceEndsAfter;
564
+ if (opts.recurrenceEndsOn !== void 0) recurrence.endsOn = opts.recurrenceEndsOn;
565
+ return recurrence;
566
+ }
567
+ function assertPixAutomaticRequirements(opts) {
568
+ if (!opts.methods.includes("pix_automatic")) return;
569
+ if (opts.type !== "recurring") {
570
+ throw new CliError(
571
+ "invalid_input",
572
+ `--methods pix_automatic requires --type=recurring (Pix Autom\xE1tico is auto-debit recurring; got '${opts.type}')`
573
+ );
574
+ }
575
+ if (opts.productId === void 0) {
576
+ throw new CliError(
577
+ "invalid_input",
578
+ "--methods pix_automatic requires --product-id (Pix Autom\xE1tico must be enabled on a product)"
579
+ );
580
+ }
581
+ }
582
+ async function scheduledChargesCreateCommand(opts) {
583
+ assertPixAutomaticRequirements(opts);
584
+ const garu = await getClient3(opts);
585
+ const params = {
586
+ customerId: opts.customerId,
587
+ amount: opts.amount,
588
+ type: opts.type,
589
+ dueDate: opts.dueDate,
590
+ methods: opts.methods
591
+ };
592
+ if (opts.productId !== void 0) params.productId = opts.productId;
593
+ if (opts.description !== void 0) params.description = opts.description;
594
+ const recurrence = buildRecurrence(opts);
595
+ if (recurrence !== void 0) params.recurrence = recurrence;
596
+ if (opts.trialDays !== void 0) params.trialDays = opts.trialDays;
597
+ if (opts.externalReference !== void 0) params.externalReference = opts.externalReference;
598
+ if (opts.metadata !== void 0) params.metadata = opts.metadata;
599
+ if (opts.maxRecoveryDays !== void 0) params.maxRecoveryDays = opts.maxRecoveryDays;
600
+ if (opts.idempotencyKey !== void 0) params.idempotencyKey = opts.idempotencyKey;
601
+ const charge = await garu.scheduledCharges.create(params);
602
+ printResult(charge, { ...opts, prettyPrint: prettyScheduledCharge });
603
+ return charge;
604
+ }
605
+ async function scheduledChargesListCommand(opts) {
606
+ const garu = await getClient3(opts);
607
+ const params = {};
608
+ if (opts.page !== void 0) params.page = opts.page;
609
+ if (opts.limit !== void 0) params.limit = opts.limit;
610
+ if (opts.customerId !== void 0) params.customerId = opts.customerId;
611
+ if (opts.status !== void 0 && opts.status.length > 0) {
612
+ params.status = opts.status.length === 1 ? opts.status[0] : opts.status;
613
+ }
614
+ if (opts.type !== void 0) params.type = opts.type;
615
+ if (opts.dueFrom !== void 0) params.dueFrom = opts.dueFrom;
616
+ if (opts.dueTo !== void 0) params.dueTo = opts.dueTo;
617
+ if (opts.search !== void 0) params.search = opts.search;
618
+ const result = await garu.scheduledCharges.list(params);
619
+ printResult(result, { ...opts, prettyPrint: prettyScheduledChargeList });
620
+ return result;
621
+ }
622
+ async function scheduledChargesGetCommand(opts) {
623
+ const garu = await getClient3(opts);
624
+ const detail = await garu.scheduledCharges.get(opts.id);
625
+ printResult(detail, { ...opts, prettyPrint: prettyScheduledChargeDetail });
626
+ return detail;
627
+ }
628
+ async function scheduledChargesPostponeCommand(opts) {
629
+ const garu = await getClient3(opts);
630
+ const params = { newDueDate: opts.newDueDate };
631
+ if (opts.reason !== void 0) params.reason = opts.reason;
632
+ const charge = await garu.scheduledCharges.postpone(opts.id, params);
633
+ printResult(charge, { ...opts, prettyPrint: prettyScheduledCharge });
634
+ return charge;
635
+ }
636
+ async function scheduledChargesPauseCommand(opts) {
637
+ const garu = await getClient3(opts);
638
+ const params = {};
639
+ if (opts.reason !== void 0) params.reason = opts.reason;
640
+ const charge = await garu.scheduledCharges.pause(opts.id, params);
641
+ printResult(charge, { ...opts, prettyPrint: prettyScheduledCharge });
642
+ return charge;
643
+ }
644
+ async function scheduledChargesResumeCommand(opts) {
645
+ const garu = await getClient3(opts);
646
+ const charge = await garu.scheduledCharges.resume(opts.id);
647
+ printResult(charge, { ...opts, prettyPrint: prettyScheduledCharge });
648
+ return charge;
649
+ }
650
+ async function scheduledChargesMarkPaidCommand(opts) {
651
+ const garu = await getClient3(opts);
652
+ const params = { paymentDate: opts.paymentDate };
653
+ if (opts.externalReference !== void 0) params.externalReference = opts.externalReference;
654
+ if (opts.cycleNumber !== void 0) params.cycleNumber = opts.cycleNumber;
655
+ const charge = await garu.scheduledCharges.markPaid(opts.id, params);
656
+ printResult(charge, { ...opts, prettyPrint: prettyScheduledCharge });
657
+ return charge;
658
+ }
659
+ async function scheduledChargesCancelRecurrenceCommand(opts) {
660
+ const garu = await getClient3(opts);
661
+ const params = {};
662
+ if (opts.reason !== void 0) params.reason = opts.reason;
663
+ const charge = await garu.scheduledCharges.cancelRecurrence(opts.id, params);
664
+ printResult(charge, { ...opts, prettyPrint: prettyScheduledCharge });
665
+ return charge;
666
+ }
667
+ async function scheduledChargesCancelAtPeriodEndCommand(opts) {
668
+ const garu = await getClient3(opts);
669
+ const params = { enabled: opts.enabled };
670
+ const charge = await garu.scheduledCharges.setCancelAtPeriodEnd(opts.id, params);
671
+ printResult(charge, { ...opts, prettyPrint: prettyScheduledCharge });
672
+ return charge;
673
+ }
674
+ async function scheduledChargesChangePaymentMethodCommand(opts) {
675
+ const garu = await getClient3(opts);
676
+ const params = {
677
+ paymentMethodId: opts.paymentMethodId
678
+ };
679
+ const charge = await garu.scheduledCharges.changePaymentMethod(opts.id, params);
680
+ printResult(charge, { ...opts, prettyPrint: prettyScheduledCharge });
681
+ return charge;
682
+ }
683
+ async function scheduledChargesClearPaymentMethodCommand(opts) {
684
+ const garu = await getClient3(opts);
685
+ const charge = await garu.scheduledCharges.clearPaymentMethod(opts.id);
686
+ printResult(charge, { ...opts, prettyPrint: prettyScheduledCharge });
687
+ return charge;
688
+ }
689
+ async function scheduledChargesAttemptsCommand(opts) {
690
+ const garu = await getClient3(opts);
691
+ const params = {};
692
+ if (opts.page !== void 0) params.page = opts.page;
693
+ if (opts.limit !== void 0) params.limit = opts.limit;
694
+ if (opts.cycleNumber !== void 0) params.cycleNumber = opts.cycleNumber;
695
+ const result = await garu.scheduledCharges.listAttempts(opts.id, params);
696
+ printResult(result, { ...opts, prettyPrint: prettyAttemptList });
697
+ return result;
698
+ }
699
+ async function scheduledChargesChargeNowCommand(opts) {
700
+ const garu = await getClient3(opts);
701
+ const result = await garu.scheduledCharges.chargeNow(opts.id);
702
+ printResult(result, { ...opts, prettyPrint: prettyChargeNow });
703
+ if (result.outcome === "failed" || result.outcome === "not_sent") {
704
+ process.exitCode = 1;
705
+ }
706
+ return result;
707
+ }
708
+ function prettyScheduledCharge(c) {
709
+ const lines = [
710
+ `Scheduled charge ${c.id}`,
711
+ ` status: ${c.status}`,
712
+ ` type: ${c.type}`,
713
+ ` amount: ${c.amount}`,
714
+ ` dueDate: ${c.dueDate}`,
715
+ ` methods: ${c.methods.join(", ")}`
716
+ ];
717
+ if (c.description) lines.push(` description: ${c.description}`);
718
+ if (c.maxRecoveryDays !== null) lines.push(` maxRecoveryDays: ${c.maxRecoveryDays}`);
719
+ if (c.externalReference) lines.push(` externalRef: ${c.externalReference}`);
720
+ return lines.join("\n");
721
+ }
722
+ function prettyScheduledChargeList(list) {
723
+ if (list.data.length === 0) {
724
+ return `No scheduled charges found (page ${list.meta.page}/${list.meta.totalPages || 1})`;
725
+ }
726
+ const header = `Scheduled charges (page ${list.meta.page}/${list.meta.totalPages || "?"}, ${list.meta.total} total)`;
727
+ const rows = list.data.map(
728
+ (c) => ` ${c.id.padEnd(20)} ${String(c.status).padEnd(18)} ${String(c.type).padEnd(9)} ${String(c.amount).padStart(10)} ${c.dueDate}`
729
+ );
730
+ return [header, ...rows].join("\n");
731
+ }
732
+ function prettyScheduledChargeDetail(detail) {
733
+ const lines = [prettyScheduledCharge(detail.charge)];
734
+ if (detail.events.length > 0) {
735
+ lines.push(" events:");
736
+ for (const e of detail.events) {
737
+ lines.push(` [${e.id}] ${e.eventType} (${e.createdAt})`);
738
+ }
739
+ }
740
+ if (detail.transactions.length > 0) {
741
+ lines.push(" transactions:");
742
+ for (const t of detail.transactions) {
743
+ lines.push(` [${t.id}] ${t.paymentMethod} ${t.status} ${t.value} (${t.date})`);
744
+ }
745
+ }
746
+ return lines.join("\n");
747
+ }
748
+ function prettyAttemptList(list) {
749
+ if (list.data.length === 0) {
750
+ return `No billing attempts found (page ${list.meta.page}/${list.meta.totalPages || 1})`;
751
+ }
752
+ const header = `Billing attempts (page ${list.meta.page}/${list.meta.totalPages || "?"}, ${list.meta.total} total)`;
753
+ const rows = list.data.map(
754
+ (a) => ` cycle ${String(a.cycleNumber).padStart(3)} #${a.attemptNumber} ${a.status.padEnd(10)} ${a.paymentMethod.padEnd(7)} ${a.failureCode ?? ""} ${a.attemptedAt}`
755
+ );
756
+ return [header, ...rows].join("\n");
757
+ }
758
+ function prettyChargeNow(result) {
759
+ const lines = [result.message, ` outcome: ${result.outcome}`];
760
+ if (result.cycleNumber !== null) lines.push(` cycle: ${result.cycleNumber}`);
761
+ if (result.reason) lines.push(` reason: ${result.reason}`);
762
+ return lines.join("\n");
763
+ }
764
+ async function getClient4(opts) {
765
+ if (opts.garu) return opts.garu;
766
+ const auth = await resolveAuth({
767
+ ...opts.apiKey !== void 0 ? { apiKey: opts.apiKey } : {},
768
+ ...opts.profile !== void 0 ? { profile: opts.profile } : {}
769
+ });
770
+ return createGaruClient({
771
+ auth,
772
+ ...opts.baseUrl !== void 0 ? { baseUrl: opts.baseUrl } : {}
773
+ });
774
+ }
775
+ async function webhooksEventsListCommand(opts) {
776
+ const garu = await getClient4(opts);
491
777
  const params = {};
492
778
  if (opts.page !== void 0) params.page = opts.page;
493
779
  if (opts.limit !== void 0) params.limit = opts.limit;
@@ -499,19 +785,19 @@ async function webhooksEventsListCommand(opts) {
499
785
  return result;
500
786
  }
501
787
  async function webhooksEventsGetCommand(opts) {
502
- const garu = await getClient2(opts);
788
+ const garu = await getClient4(opts);
503
789
  const event = await garu.webhookEvents.get(opts.id);
504
790
  printResult(event, { ...opts, prettyPrint: prettyWebhookEvent });
505
791
  return event;
506
792
  }
507
793
  async function webhooksEventsRetryCommand(opts) {
508
- const garu = await getClient2(opts);
794
+ const garu = await getClient4(opts);
509
795
  const event = await garu.webhookEvents.retry(opts.id);
510
796
  printResult(event, { ...opts, prettyPrint: prettyWebhookEvent });
511
797
  return event;
512
798
  }
513
799
  async function webhooksEventsResendCommand(opts) {
514
- const garu = await getClient2(opts);
800
+ const garu = await getClient4(opts);
515
801
  const clone = await garu.webhookEvents.resend(opts.id);
516
802
  printSuccess(`Resent event ${opts.id} \u2192 new event ${clone.id}`, opts);
517
803
  printResult(clone, { ...opts, prettyPrint: prettyWebhookEvent });
@@ -575,11 +861,138 @@ function parseWebhookEventStatus(raw) {
575
861
  `--status must be 'pending', 'success', or 'failed' (got '${raw}')`
576
862
  );
577
863
  }
864
+ function parseScheduledChargeType(raw) {
865
+ if (raw === "one_time" || raw === "recurring") return raw;
866
+ throw new CliError("invalid_input", `--type must be 'one_time' or 'recurring' (got '${raw}')`);
867
+ }
868
+ var SCHEDULED_PAYMENT_METHODS = [
869
+ "pix",
870
+ "boleto",
871
+ "card",
872
+ "pix_automatic"
873
+ ];
874
+ function parseCsvList(raw) {
875
+ return raw.split(",").map((s) => s.trim()).filter(Boolean);
876
+ }
877
+ function parseScheduledPaymentMethods(raw) {
878
+ const parts = parseCsvList(raw);
879
+ if (parts.length === 0) {
880
+ throw new CliError(
881
+ "invalid_input",
882
+ `--methods must list at least one of: ${SCHEDULED_PAYMENT_METHODS.join(", ")}`
883
+ );
884
+ }
885
+ for (const p of parts) {
886
+ if (!SCHEDULED_PAYMENT_METHODS.includes(p)) {
887
+ throw new CliError(
888
+ "invalid_input",
889
+ `--methods entries must be one of ${SCHEDULED_PAYMENT_METHODS.join(", ")} (got '${p}')`
890
+ );
891
+ }
892
+ }
893
+ return parts;
894
+ }
895
+ var RECURRENCE_INTERVALS = [
896
+ "weekly",
897
+ "biweekly",
898
+ "monthly",
899
+ "bimonthly",
900
+ "quarterly",
901
+ "biannual",
902
+ "yearly"
903
+ ];
904
+ function parseRecurrenceInterval(raw) {
905
+ if (RECURRENCE_INTERVALS.includes(raw)) return raw;
906
+ throw new CliError(
907
+ "invalid_input",
908
+ `--recurrence-interval must be one of ${RECURRENCE_INTERVALS.join(", ")} (got '${raw}')`
909
+ );
910
+ }
911
+ var SCHEDULED_CHARGE_STATUSES = [
912
+ "scheduled",
913
+ "due_today",
914
+ "overdue",
915
+ "paid",
916
+ "paused",
917
+ "canceled",
918
+ "trial",
919
+ "pending_tokenization",
920
+ "recurrence_canceled"
921
+ ];
922
+ function parseScheduledChargeStatus(raw) {
923
+ if (SCHEDULED_CHARGE_STATUSES.includes(raw)) return raw;
924
+ throw new CliError(
925
+ "invalid_input",
926
+ `--status must be one of ${SCHEDULED_CHARGE_STATUSES.join(", ")} (got '${raw}')`
927
+ );
928
+ }
929
+ function parseIntInRange(raw, label, min, max) {
930
+ const trimmed = raw.trim();
931
+ const n = Number.parseInt(trimmed, 10);
932
+ if (!Number.isFinite(n) || String(n) !== trimmed || n < min || n > max) {
933
+ throw new CliError(
934
+ "invalid_input",
935
+ `${label} must be an integer between ${min} and ${max} (got '${raw}')`
936
+ );
937
+ }
938
+ return n;
939
+ }
940
+ function parseNonNegativeInt(raw, label) {
941
+ const trimmed = raw.trim();
942
+ const n = Number.parseInt(trimmed, 10);
943
+ if (!Number.isFinite(n) || String(n) !== trimmed || n < 0) {
944
+ throw new CliError("invalid_input", `${label} must be a non-negative integer (got '${raw}')`);
945
+ }
946
+ return n;
947
+ }
948
+ function parseAmountBrl(raw) {
949
+ const n = Number(raw);
950
+ if (!Number.isFinite(n) || n <= 0) {
951
+ throw new CliError(
952
+ "invalid_input",
953
+ `--amount must be a positive decimal in BRL, e.g. 297.50 (got '${raw}')`
954
+ );
955
+ }
956
+ return n;
957
+ }
958
+ function parseMetadata(raw) {
959
+ let parsed;
960
+ try {
961
+ parsed = JSON.parse(raw);
962
+ } catch {
963
+ throw new CliError("invalid_input", `--metadata must be valid JSON (got '${raw}')`);
964
+ }
965
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
966
+ throw new CliError("invalid_input", `--metadata must be a JSON object`);
967
+ }
968
+ return parsed;
969
+ }
578
970
 
579
971
  // src/index.ts
580
972
  function buildCli() {
581
973
  const program = new commander.Command();
582
- program.name("garu").description("Command-line interface for the Garu payment gateway.").version(CLI_VERSION, "-v, --version").addOption(new commander.Option("--api-key <key>", "Garu API key (overrides env and credentials file)")).addOption(new commander.Option("-p, --profile <name>", "credentials profile name")).addOption(new commander.Option("--json", "emit strict JSON on stdout (forced in pipes)")).addOption(new commander.Option("-q, --quiet", "suppress status output; only print results and errors")).showHelpAfterError();
974
+ program.name("garu").description("Command-line interface for the Garu payment gateway.").version(CLI_VERSION, "-v, --version").addOption(new commander.Option("--api-key <key>", "Garu API key (overrides env and credentials file)")).addOption(new commander.Option("-p, --profile <name>", "credentials profile name")).addOption(new commander.Option("--json", "emit strict JSON on stdout (forced in pipes)")).addOption(new commander.Option("-q, --quiet", "suppress status output; only print results and errors")).showHelpAfterError().addHelpText(
975
+ "after",
976
+ `
977
+ Recipes:
978
+ Pix Autom\xE1tico recurring subscription (end-to-end):
979
+
980
+ # 1. Create a product with Pix Autom\xE1tico enabled
981
+ garu products create \\
982
+ --name "Plano Mensal" --value 4990 \\
983
+ --pix --credit-card --pix-automatic \\
984
+ --subscription --subscription-type monthly
985
+
986
+ # 2. Schedule the recurring auto-debit charge for that product.
987
+ # pix_automatic requires --type=recurring and --product-id.
988
+ garu scheduled-charges create \\
989
+ --customer-id 42 --product-id 456 \\
990
+ --amount 49.90 --type recurring \\
991
+ --due-date 2026-06-15 \\
992
+ --methods pix_automatic \\
993
+ --recurrence-interval monthly
994
+ `
995
+ );
583
996
  program.command("login").description("Save a Garu API key to the credentials file").option("--api-key <key>", "pre-supply the key instead of prompting").option("-p, --profile <name>", "profile name to store under", "default").action(async (cmdOpts) => {
584
997
  const base = toCommandOptions(program);
585
998
  await loginCommand({
@@ -605,17 +1018,19 @@ function buildCli() {
605
1018
  );
606
1019
  });
607
1020
  const charges = program.command("charges").description("Create, fetch, and refund charges");
608
- charges.command("list").description("List charges with pagination and filters").option("--page <n>", "page number (1-based)", (v) => parseInt(v, 10)).option("--limit <n>", "items per page (1-100)", (v) => parseInt(v, 10)).option("--status <status>", "filter by status (e.g. paid, pending)").option("--search <query>", "search by customer name, email, or document").option("--payment-method <method>", "filter: pix, creditcard, boleto").action(async (cmdOpts) => {
609
- const base = toCommandOptions(program);
610
- await chargesListCommand({
611
- ...base,
612
- page: cmdOpts.page,
613
- limit: cmdOpts.limit,
614
- status: cmdOpts.status,
615
- search: cmdOpts.search,
616
- paymentMethod: cmdOpts.paymentMethod
617
- }).catch((err) => printErrorAndExit(err, base));
618
- });
1021
+ charges.command("list").description("List charges with pagination and filters").option("--page <n>", "page number (1-based)", (v) => parseInt(v, 10)).option("--limit <n>", "items per page (1-100)", (v) => parseInt(v, 10)).option("--status <status>", "filter by status (e.g. paid, pending)").option("--search <query>", "search by customer name, email, or document").option("--payment-method <method>", "filter: pix, creditcard, boleto").action(
1022
+ async (cmdOpts) => {
1023
+ const base = toCommandOptions(program);
1024
+ await chargesListCommand({
1025
+ ...base,
1026
+ page: cmdOpts.page,
1027
+ limit: cmdOpts.limit,
1028
+ status: cmdOpts.status,
1029
+ search: cmdOpts.search,
1030
+ paymentMethod: cmdOpts.paymentMethod
1031
+ }).catch((err) => printErrorAndExit(err, base));
1032
+ }
1033
+ );
619
1034
  charges.command("create").description("Create a charge (PIX, credit card, or boleto)").requiredOption("--type <type>", "payment method: pix | credit_card | boleto").requiredOption("--product-id <uuid>", "product UUID").requiredOption("--customer-name <name>", "customer full name").requiredOption("--customer-email <email>", "customer email").requiredOption(
620
1035
  "--customer-document <document>",
621
1036
  "CPF (11 digits) or CNPJ (14 digits), digits only"
@@ -654,6 +1069,176 @@ function buildCli() {
654
1069
  idempotencyKey: cmdOpts.idempotencyKey
655
1070
  }).catch((err) => printErrorAndExit(err, base));
656
1071
  });
1072
+ const scheduled = program.command("scheduled-charges").description("Schedule, inspect, and act on future-dated charges");
1073
+ scheduled.command("create").description("Schedule a future-dated charge (one-time or recurring)").requiredOption(
1074
+ "--customer-id <n>",
1075
+ "customer id",
1076
+ (v) => parsePositiveIntId(v, "--customer-id")
1077
+ ).requiredOption("--amount <brl>", "decimal BRL amount, e.g. 297.50").requiredOption("--type <type>", "one_time | recurring").requiredOption("--due-date <yyyy-mm-dd>", "first due date in S\xE3o Paulo time").requiredOption("--methods <list>", "comma-separated: pix,boleto,card,pix_automatic").option(
1078
+ "--product-id <n>",
1079
+ "product id (required when methods includes card or pix_automatic)",
1080
+ (v) => parsePositiveIntId(v, "--product-id")
1081
+ ).option("--description <text>", "charge description").option("--recurrence-interval <interval>", "recurring cadence: weekly|monthly|yearly|\u2026").option(
1082
+ "--recurrence-interval-count <n>",
1083
+ "multiplier for the interval",
1084
+ (v) => parsePositiveIntId(v, "--recurrence-interval-count")
1085
+ ).option(
1086
+ "--recurrence-ends-after <n>",
1087
+ "stop after N successful cycles",
1088
+ (v) => parsePositiveIntId(v, "--recurrence-ends-after")
1089
+ ).option("--recurrence-ends-on <yyyy-mm-dd>", "stop after this calendar date").option("--trial-days <n>", "free-trial days (1-365, recurring-only)").option("--external-reference <ref>", "your own reconciliation reference").option("--metadata <json>", "JSON object of custom metadata").option(
1090
+ "--max-recovery-days <n>",
1091
+ "days past due the recovery sweep keeps auto-billing (1-365; default 14)"
1092
+ ).option("--idempotency-key <key>", "idempotency key (auto-generated if omitted)").action(async (cmdOpts) => {
1093
+ const base = toCommandOptions(program);
1094
+ await scheduledChargesCreateCommand({
1095
+ ...base,
1096
+ customerId: cmdOpts.customerId,
1097
+ amount: parseAmountBrl(cmdOpts.amount),
1098
+ type: parseScheduledChargeType(cmdOpts.type),
1099
+ dueDate: cmdOpts.dueDate,
1100
+ methods: parseScheduledPaymentMethods(cmdOpts.methods),
1101
+ productId: cmdOpts.productId,
1102
+ description: cmdOpts.description,
1103
+ recurrenceInterval: cmdOpts.recurrenceInterval ? parseRecurrenceInterval(cmdOpts.recurrenceInterval) : void 0,
1104
+ recurrenceIntervalCount: cmdOpts.recurrenceIntervalCount,
1105
+ recurrenceEndsAfter: cmdOpts.recurrenceEndsAfter,
1106
+ recurrenceEndsOn: cmdOpts.recurrenceEndsOn,
1107
+ trialDays: cmdOpts.trialDays !== void 0 ? parseIntInRange(cmdOpts.trialDays, "--trial-days", 1, 365) : void 0,
1108
+ externalReference: cmdOpts.externalReference,
1109
+ metadata: cmdOpts.metadata ? parseMetadata(cmdOpts.metadata) : void 0,
1110
+ maxRecoveryDays: cmdOpts.maxRecoveryDays !== void 0 ? parseIntInRange(cmdOpts.maxRecoveryDays, "--max-recovery-days", 1, 365) : void 0,
1111
+ idempotencyKey: cmdOpts.idempotencyKey
1112
+ }).catch((err) => printErrorAndExit(err, base));
1113
+ });
1114
+ scheduled.command("list").description("List scheduled charges with pagination and filters").option("--page <n>", "page number (1-based)", (v) => parseInt(v, 10)).option("--limit <n>", "items per page (1-100)", (v) => parseInt(v, 10)).option(
1115
+ "--customer-id <n>",
1116
+ "filter by customer id",
1117
+ (v) => parsePositiveIntId(v, "--customer-id")
1118
+ ).option(
1119
+ "--status <status>",
1120
+ "filter by status (repeatable)",
1121
+ (val, acc) => acc.concat(val),
1122
+ []
1123
+ ).option("--type <type>", "filter by type: one_time | recurring").option("--due-from <yyyy-mm-dd>", "lower bound for due date").option("--due-to <yyyy-mm-dd>", "upper bound for due date").option("--search <query>", "search by customer name, email, or document").action(
1124
+ async (cmdOpts) => {
1125
+ const base = toCommandOptions(program);
1126
+ await scheduledChargesListCommand({
1127
+ ...base,
1128
+ page: cmdOpts.page,
1129
+ limit: cmdOpts.limit,
1130
+ customerId: cmdOpts.customerId,
1131
+ status: cmdOpts.status.length > 0 ? cmdOpts.status.map(parseScheduledChargeStatus) : void 0,
1132
+ type: cmdOpts.type ? parseScheduledChargeType(cmdOpts.type) : void 0,
1133
+ dueFrom: cmdOpts.dueFrom,
1134
+ dueTo: cmdOpts.dueTo,
1135
+ search: cmdOpts.search
1136
+ }).catch((err) => printErrorAndExit(err, base));
1137
+ }
1138
+ );
1139
+ scheduled.command("get <id>").description("Fetch a scheduled charge with its event timeline and linked transactions").action(async (id) => {
1140
+ const base = toCommandOptions(program);
1141
+ await scheduledChargesGetCommand({ ...base, id }).catch(
1142
+ (err) => printErrorAndExit(err, base)
1143
+ );
1144
+ });
1145
+ scheduled.command("charge-now <id>").description(
1146
+ "Dispatch a scheduled charge now (the charge + notification the daily cron would send on the due date), instead of waiting for dueDate. Idempotent: an already-dispatched cycle reports `already_sent` and is NOT re-charged. Prints the result message; exits non-zero on outcome failed/not_sent (and on 4xx)."
1147
+ ).action(async (id) => {
1148
+ const base = toCommandOptions(program);
1149
+ await scheduledChargesChargeNowCommand({ ...base, id }).catch(
1150
+ (err) => printErrorAndExit(err, base)
1151
+ );
1152
+ });
1153
+ scheduled.command("postpone <id>").description("Move a scheduled charge to a new due date").requiredOption("--new-due-date <yyyy-mm-dd>", "new due date (today or future)").option("--reason <text>", "optional reason recorded in the timeline").action(async (id, cmdOpts) => {
1154
+ const base = toCommandOptions(program);
1155
+ await scheduledChargesPostponeCommand({
1156
+ ...base,
1157
+ id,
1158
+ newDueDate: cmdOpts.newDueDate,
1159
+ reason: cmdOpts.reason
1160
+ }).catch((err) => printErrorAndExit(err, base));
1161
+ });
1162
+ scheduled.command("pause <id>").description("Pause a scheduled charge (no reminders fire while paused)").option("--reason <text>", "optional reason recorded in the timeline").action(async (id, cmdOpts) => {
1163
+ const base = toCommandOptions(program);
1164
+ await scheduledChargesPauseCommand({ ...base, id, reason: cmdOpts.reason }).catch(
1165
+ (err) => printErrorAndExit(err, base)
1166
+ );
1167
+ });
1168
+ scheduled.command("resume <id>").description("Resume a paused scheduled charge").action(async (id) => {
1169
+ const base = toCommandOptions(program);
1170
+ await scheduledChargesResumeCommand({ ...base, id }).catch(
1171
+ (err) => printErrorAndExit(err, base)
1172
+ );
1173
+ });
1174
+ scheduled.command("mark-paid <id>").description("Mark a scheduled charge as paid out-of-band (bank transfer, cash, etc.)").requiredOption("--payment-date <yyyy-mm-dd>", "date the payment was received (today or past)").option("--external-reference <ref>", "bank reference or internal id for reconciliation").option(
1175
+ "--cycle-number <n>",
1176
+ "cycle to mark paid (REQUIRED for recurring)",
1177
+ (v) => parsePositiveIntId(v, "--cycle-number")
1178
+ ).action(
1179
+ async (id, cmdOpts) => {
1180
+ const base = toCommandOptions(program);
1181
+ await scheduledChargesMarkPaidCommand({
1182
+ ...base,
1183
+ id,
1184
+ paymentDate: cmdOpts.paymentDate,
1185
+ externalReference: cmdOpts.externalReference,
1186
+ cycleNumber: cmdOpts.cycleNumber
1187
+ }).catch((err) => printErrorAndExit(err, base));
1188
+ }
1189
+ );
1190
+ scheduled.command("cancel-recurrence <id>").description("Stop future cycles of a recurring series (recurring-only)").option("--reason <text>", "optional reason recorded in the timeline").action(async (id, cmdOpts) => {
1191
+ const base = toCommandOptions(program);
1192
+ await scheduledChargesCancelRecurrenceCommand({
1193
+ ...base,
1194
+ id,
1195
+ reason: cmdOpts.reason
1196
+ }).catch((err) => printErrorAndExit(err, base));
1197
+ });
1198
+ scheduled.command("cancel-at-period-end <id>").description("Toggle Stripe-style soft cancel on a recurring series (recurring-only)").option("--disable", "clear the flag instead of setting it").action(async (id, cmdOpts) => {
1199
+ const base = toCommandOptions(program);
1200
+ await scheduledChargesCancelAtPeriodEndCommand({
1201
+ ...base,
1202
+ id,
1203
+ enabled: !cmdOpts.disable
1204
+ }).catch((err) => printErrorAndExit(err, base));
1205
+ });
1206
+ scheduled.command("change-payment-method <id>").description("Swap the saved card on a recurring series (recurring-only)").requiredOption(
1207
+ "--payment-method-id <n>",
1208
+ "PaymentMethod id (same customer)",
1209
+ (v) => parsePositiveIntId(v, "--payment-method-id")
1210
+ ).action(async (id, cmdOpts) => {
1211
+ const base = toCommandOptions(program);
1212
+ await scheduledChargesChangePaymentMethodCommand({
1213
+ ...base,
1214
+ id,
1215
+ paymentMethodId: cmdOpts.paymentMethodId
1216
+ }).catch((err) => printErrorAndExit(err, base));
1217
+ });
1218
+ scheduled.command("clear-payment-method <id>").description(
1219
+ "Clear the saved card; future cycles fall back to email-with-link (recurring-only)"
1220
+ ).action(async (id) => {
1221
+ const base = toCommandOptions(program);
1222
+ await scheduledChargesClearPaymentMethodCommand({ ...base, id }).catch(
1223
+ (err) => printErrorAndExit(err, base)
1224
+ );
1225
+ });
1226
+ scheduled.command("attempts <id>").description("List the per-attempt billing log for a scheduled charge").option("--page <n>", "page number (1-based)", (v) => parseInt(v, 10)).option("--limit <n>", "items per page (1-100)", (v) => parseInt(v, 10)).option(
1227
+ "--cycle-number <n>",
1228
+ "filter to a single cycle",
1229
+ (v) => parsePositiveIntId(v, "--cycle-number")
1230
+ ).action(
1231
+ async (id, cmdOpts) => {
1232
+ const base = toCommandOptions(program);
1233
+ await scheduledChargesAttemptsCommand({
1234
+ ...base,
1235
+ id,
1236
+ page: cmdOpts.page,
1237
+ limit: cmdOpts.limit,
1238
+ cycleNumber: cmdOpts.cycleNumber
1239
+ }).catch((err) => printErrorAndExit(err, base));
1240
+ }
1241
+ );
657
1242
  const webhooks = program.command("webhooks").description("Inspect and replay webhook deliveries");
658
1243
  const events = webhooks.command("events").description("List, inspect, and resend webhook events");
659
1244
  events.command("list").description("List webhook events with filters").option("--page <n>", "page number (1-based)", (v) => parseInt(v, 10)).option("--limit <n>", "items per page (1-100)", (v) => parseInt(v, 10)).option("--status <status>", "filter: pending, success, or failed").option("--event-type <type>", "filter by Garu event type, e.g. transaction.payment.paid").option(
@@ -698,6 +1283,68 @@ function buildCli() {
698
1283
  id: parsePositiveIntId(id, "Webhook event ID")
699
1284
  }).catch((err) => printErrorAndExit(err, base));
700
1285
  });
1286
+ const products = program.command("products").description("Create and update products");
1287
+ products.command("create").description("Create a product").requiredOption("--name <name>", "product name").option(
1288
+ "--value <centavos>",
1289
+ "price in centavos (BRL \xD7 100)",
1290
+ (v) => parseNonNegativeInt(v, "--value")
1291
+ ).option("--description <text>", "product description").option("--image <url>", "HTTPS URL of the product cover image").option("--tags <list>", "comma-separated tags", parseCsvList).option("--pix", "accept PIX").option("--no-pix", "do not accept PIX").option("--boleto", "accept boleto").option("--no-boleto", "do not accept boleto").option("--credit-card", "accept credit card").option("--no-credit-card", "do not accept credit card").option("--pix-automatic", "expose Pix Autom\xE1tico on the subscription checkout").option("--no-pix-automatic", "do not expose Pix Autom\xE1tico").option(
1292
+ "--installments <n>",
1293
+ "max credit-card installments",
1294
+ (v) => parsePositiveIntId(v, "--installments")
1295
+ ).option("--subscription", "mark the product as a subscription").option("--no-subscription", "mark the product as one-time").option("--subscription-type <type>", "subscription cadence, e.g. monthly").option("--unit-label <label>", "unit label shown on the checkout").option("--return-url <url>", "post-purchase redirect URL").option("--return-url-button-text <text>", "label for the return-URL button").action(async (cmdOpts) => {
1296
+ const base = toCommandOptions(program);
1297
+ await productsCreateCommand({
1298
+ ...base,
1299
+ name: cmdOpts.name,
1300
+ value: cmdOpts.value,
1301
+ description: cmdOpts.description,
1302
+ image: cmdOpts.image,
1303
+ tags: cmdOpts.tags,
1304
+ pix: cmdOpts.pix,
1305
+ boleto: cmdOpts.boleto,
1306
+ creditCard: cmdOpts.creditCard,
1307
+ pixAutomatic: cmdOpts.pixAutomatic,
1308
+ installments: cmdOpts.installments,
1309
+ isSubscription: cmdOpts.subscription,
1310
+ subscriptionType: cmdOpts.subscriptionType,
1311
+ unitLabel: cmdOpts.unitLabel,
1312
+ returnUrl: cmdOpts.returnUrl,
1313
+ returnUrlButtonText: cmdOpts.returnUrlButtonText
1314
+ }).catch((err) => printErrorAndExit(err, base));
1315
+ });
1316
+ products.command("update <id>").description(
1317
+ "Update a product (partial \u2014 only the flags you pass change). <id> is the numeric id or UUID"
1318
+ ).option("--name <name>", "product name").option(
1319
+ "--value <centavos>",
1320
+ "price in centavos (BRL \xD7 100)",
1321
+ (v) => parseNonNegativeInt(v, "--value")
1322
+ ).option("--description <text>", "product description").option("--image <url>", "HTTPS URL of the product cover image").option("--tags <list>", "comma-separated tags", parseCsvList).option("--pix", "accept PIX").option("--no-pix", "do not accept PIX").option("--boleto", "accept boleto").option("--no-boleto", "do not accept boleto").option("--credit-card", "accept credit card").option("--no-credit-card", "do not accept credit card").option("--pix-automatic", "expose Pix Autom\xE1tico on the subscription checkout").option("--no-pix-automatic", "do not expose Pix Autom\xE1tico").option(
1323
+ "--installments <n>",
1324
+ "max credit-card installments",
1325
+ (v) => parsePositiveIntId(v, "--installments")
1326
+ ).option("--subscription", "mark the product as a subscription").option("--no-subscription", "mark the product as one-time").option("--subscription-type <type>", "subscription cadence, e.g. monthly").option("--unit-label <label>", "unit label shown on the checkout").option("--return-url <url>", "post-purchase redirect URL").option("--return-url-button-text <text>", "label for the return-URL button").action(async (id, cmdOpts) => {
1327
+ const base = toCommandOptions(program);
1328
+ await productsUpdateCommand({
1329
+ ...base,
1330
+ id,
1331
+ name: cmdOpts.name,
1332
+ value: cmdOpts.value,
1333
+ description: cmdOpts.description,
1334
+ image: cmdOpts.image,
1335
+ tags: cmdOpts.tags,
1336
+ pix: cmdOpts.pix,
1337
+ boleto: cmdOpts.boleto,
1338
+ creditCard: cmdOpts.creditCard,
1339
+ pixAutomatic: cmdOpts.pixAutomatic,
1340
+ installments: cmdOpts.installments,
1341
+ isSubscription: cmdOpts.subscription,
1342
+ subscriptionType: cmdOpts.subscriptionType,
1343
+ unitLabel: cmdOpts.unitLabel,
1344
+ returnUrl: cmdOpts.returnUrl,
1345
+ returnUrlButtonText: cmdOpts.returnUrlButtonText
1346
+ }).catch((err) => printErrorAndExit(err, base));
1347
+ });
701
1348
  program.command("doctor").description("Environment diagnostic").action(async () => {
702
1349
  const base = toCommandOptions(program);
703
1350
  await doctorCommand(base).catch((err) => printErrorAndExit(err, base));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@garuhq/cli",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Official command-line interface for the Garu payment gateway.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://garu.com.br",
@@ -46,7 +46,7 @@
46
46
  "prepublishOnly": "npm run check:version-sync && npm run typecheck && npm test && npm run build"
47
47
  },
48
48
  "dependencies": {
49
- "@garuhq/node": "0.12.0",
49
+ "@garuhq/node": "0.15.0",
50
50
  "@inquirer/prompts": "8.4.1",
51
51
  "commander": "12.0.0",
52
52
  "picocolors": "1.0.0",