fresh-squeezy 0.1.19 → 0.1.20
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/README.md +6 -1
- package/dist/cli.js +193 -10
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -137,6 +137,11 @@ npx fresh-squeezy create webhook --body-file webhook.json --mode test
|
|
|
137
137
|
npx fresh-squeezy cancel subscription --id 9 --yes
|
|
138
138
|
npx fresh-squeezy refund order --id 100 --yes --mode live
|
|
139
139
|
npx fresh-squeezy generate-invoice order --id 100 --yes
|
|
140
|
+
|
|
141
|
+
# Bulk delete / cancel / refund: --all, --ids, --match, --dry-run
|
|
142
|
+
npx fresh-squeezy delete discount --all --store-ids 12 --dry-run
|
|
143
|
+
npx fresh-squeezy delete discount --all --store-ids 12 --match TEST --yes
|
|
144
|
+
npx fresh-squeezy delete webhook --ids 1,2,3 --yes
|
|
140
145
|
```
|
|
141
146
|
|
|
142
147
|
**Safety:** `delete` / `cancel` / `refund` always require `--yes` or a TTY confirm. Live-mode writes need `--yes` or TTY confirm too. Bodies are JSON:API documents via `--body` or `--body-file` (not flat flags).
|
|
@@ -206,7 +211,7 @@ The CLI default is `--mode test`. Override with `--mode live`. Guided setup asks
|
|
|
206
211
|
| `LEMON_SQUEEZY_MODE` | no | `test` (default) or `live` |
|
|
207
212
|
| `LEMON_SQUEEZY_STORE_ID` | no | Convenience default for `client.doctor()` — library only |
|
|
208
213
|
|
|
209
|
-
|
|
214
|
+
`doctor` and `validate` do not read `LEMON_SQUEEZY_STORE_ID`; use `--store-ids` or `--all-stores` there. Only the `ops` verbs (`list`, `delete --all`, …) fall back to it when `--store-ids` is omitted.
|
|
210
215
|
|
|
211
216
|
## Issue codes
|
|
212
217
|
|
package/dist/cli.js
CHANGED
|
@@ -2947,6 +2947,11 @@ var renderCommandExamples = () => {
|
|
|
2947
2947
|
" fresh-squeezy validate connection",
|
|
2948
2948
|
" fresh-squeezy validate webhook --store-ids 12 --webhook-url https://app.example.com/api/webhooks/lemon-squeezy",
|
|
2949
2949
|
"",
|
|
2950
|
+
`${chalk2.bold("Bulk ops (delete / cancel / refund)")}`,
|
|
2951
|
+
" fresh-squeezy delete discount --all --store-ids 12 --dry-run",
|
|
2952
|
+
" fresh-squeezy delete discount --all --store-ids 12 --match TEST --yes",
|
|
2953
|
+
" fresh-squeezy delete webhook --ids 1,2,3 --yes",
|
|
2954
|
+
"",
|
|
2950
2955
|
`${chalk2.bold("Automation")}`,
|
|
2951
2956
|
" fresh-squeezy doctor --all-stores --json",
|
|
2952
2957
|
""
|
|
@@ -3364,6 +3369,94 @@ var renderProjectInstallResult = (result) => {
|
|
|
3364
3369
|
|
|
3365
3370
|
// src/cli/commands/resourceOps.ts
|
|
3366
3371
|
import { readFileSync } from "fs";
|
|
3372
|
+
|
|
3373
|
+
// src/cli/commands/bulkOps.ts
|
|
3374
|
+
var supportsBulk = (spec) => Boolean(spec.destructive) && spec.idRole === "id";
|
|
3375
|
+
var describeBulkTarget = (item) => {
|
|
3376
|
+
const attrs = item.attributes ?? {};
|
|
3377
|
+
const id = String(item.id);
|
|
3378
|
+
for (const key of ["code", "name", "url", "identifier", "user_email", "status"]) {
|
|
3379
|
+
const value = attrs[key];
|
|
3380
|
+
if (typeof value === "string" && value.length > 0) return { id, label: value };
|
|
3381
|
+
}
|
|
3382
|
+
return { id, label: id };
|
|
3383
|
+
};
|
|
3384
|
+
var matchesText = (item, text) => {
|
|
3385
|
+
const needle = text.toLowerCase();
|
|
3386
|
+
return Object.values(item.attributes ?? {}).some(
|
|
3387
|
+
(value) => typeof value === "string" && value.toLowerCase().includes(needle)
|
|
3388
|
+
);
|
|
3389
|
+
};
|
|
3390
|
+
var collectBulkTargets = async (http, spec, selector) => {
|
|
3391
|
+
if (selector.ids?.length) {
|
|
3392
|
+
return selector.ids.map((id) => ({ id, label: id }));
|
|
3393
|
+
}
|
|
3394
|
+
const listSpec = findResourceVerb(spec.resource, "list");
|
|
3395
|
+
if (!listSpec) {
|
|
3396
|
+
throw new FreshSqueezyError({
|
|
3397
|
+
code: "UNKNOWN_OP",
|
|
3398
|
+
message: `Cannot use --all: ${spec.resource} has no list op. Pass --ids instead.`
|
|
3399
|
+
});
|
|
3400
|
+
}
|
|
3401
|
+
if (listSpec.idRole === "store" && !selector.storeId) {
|
|
3402
|
+
throw new FreshSqueezyError({
|
|
3403
|
+
code: "MISSING_ARG",
|
|
3404
|
+
message: "--all needs a store: pass --store-ids or set LEMON_SQUEEZY_STORE_ID."
|
|
3405
|
+
});
|
|
3406
|
+
}
|
|
3407
|
+
if (listSpec.idRole === "parent" && !selector.parentId) {
|
|
3408
|
+
throw new FreshSqueezyError({
|
|
3409
|
+
code: "MISSING_ARG",
|
|
3410
|
+
message: `--all needs --parent-id to list ${spec.resource} items.`
|
|
3411
|
+
});
|
|
3412
|
+
}
|
|
3413
|
+
const items = await invokeOp(http, spec.resource, "list", {
|
|
3414
|
+
storeId: selector.storeId,
|
|
3415
|
+
parentId: selector.parentId
|
|
3416
|
+
});
|
|
3417
|
+
return items.filter((item) => selector.match ? matchesText(item, selector.match) : true).map(describeBulkTarget);
|
|
3418
|
+
};
|
|
3419
|
+
var executeBulk = async (http, spec, targets, opts = {}) => {
|
|
3420
|
+
const dryRun = Boolean(opts.dryRun);
|
|
3421
|
+
const results = [];
|
|
3422
|
+
for (const target of targets) {
|
|
3423
|
+
if (dryRun) {
|
|
3424
|
+
results.push({ ...target, ok: true });
|
|
3425
|
+
continue;
|
|
3426
|
+
}
|
|
3427
|
+
try {
|
|
3428
|
+
await invokeOp(http, spec.resource, spec.verb, { id: target.id, body: opts.body });
|
|
3429
|
+
results.push({ ...target, ok: true });
|
|
3430
|
+
} catch (err) {
|
|
3431
|
+
results.push({
|
|
3432
|
+
...target,
|
|
3433
|
+
ok: false,
|
|
3434
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3435
|
+
});
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
3438
|
+
const failed = results.filter((r) => !r.ok).length;
|
|
3439
|
+
return { dryRun, total: results.length, succeeded: results.length - failed, failed, results };
|
|
3440
|
+
};
|
|
3441
|
+
var previewTargets = (targets, max = 5) => {
|
|
3442
|
+
const shown = targets.slice(0, max).map((t) => t.label);
|
|
3443
|
+
const more = targets.length - shown.length;
|
|
3444
|
+
return more > 0 ? `${shown.join(", ")}, \u2026 +${more} more` : shown.join(", ");
|
|
3445
|
+
};
|
|
3446
|
+
var renderBulkSummary = (spec, summary) => {
|
|
3447
|
+
if (summary.total === 0) return `No ${spec.resource} items matched \u2014 nothing to ${spec.verb}.
|
|
3448
|
+
`;
|
|
3449
|
+
const verb = summary.dryRun ? `would ${spec.verb}` : spec.verb;
|
|
3450
|
+
const lines = summary.results.map(
|
|
3451
|
+
(r) => r.ok ? ` \u2713 ${verb} ${spec.resource} ${r.id} (${r.label})` : ` \u2717 ${spec.verb} ${spec.resource} ${r.id} (${r.label}): ${r.error}`
|
|
3452
|
+
);
|
|
3453
|
+
const tail = summary.dryRun ? `dry run \u2014 ${summary.total} ${spec.resource} item(s) would be affected, nothing sent` : `${summary.succeeded}/${summary.total} succeeded${summary.failed ? `, ${summary.failed} failed` : ""}`;
|
|
3454
|
+
return `${lines.join("\n")}
|
|
3455
|
+
${tail}
|
|
3456
|
+
`;
|
|
3457
|
+
};
|
|
3458
|
+
|
|
3459
|
+
// src/cli/commands/resourceOps.ts
|
|
3367
3460
|
var runResourceOpCommand = async (options) => {
|
|
3368
3461
|
if (options.listCatalog) {
|
|
3369
3462
|
writeCatalog(options.json ?? false);
|
|
@@ -3380,6 +3473,14 @@ var runResourceOpCommand = async (options) => {
|
|
|
3380
3473
|
);
|
|
3381
3474
|
return 2;
|
|
3382
3475
|
}
|
|
3476
|
+
const bulkError = validateBulkFlags(spec, options);
|
|
3477
|
+
if (bulkError) {
|
|
3478
|
+
writeFatal2(
|
|
3479
|
+
new FreshSqueezyError({ code: "INVALID_ARGS", message: bulkError }),
|
|
3480
|
+
options.json ?? false
|
|
3481
|
+
);
|
|
3482
|
+
return 2;
|
|
3483
|
+
}
|
|
3383
3484
|
try {
|
|
3384
3485
|
const body = readBody(options);
|
|
3385
3486
|
if (spec.body === "required" && body === void 0) {
|
|
@@ -3389,13 +3490,17 @@ var runResourceOpCommand = async (options) => {
|
|
|
3389
3490
|
});
|
|
3390
3491
|
}
|
|
3391
3492
|
const client = createFreshSqueezy({ mode: options.mode });
|
|
3392
|
-
const allowed = await assertWriteSafety(spec, client.mode, options);
|
|
3393
|
-
if (!allowed) return 2;
|
|
3394
3493
|
const config = resolveConfig({ mode: options.mode });
|
|
3395
3494
|
const http = new HttpClient(config);
|
|
3495
|
+
const storeId = options.storeIds?.[0] ?? config.storeId;
|
|
3496
|
+
if (options.all || options.ids) {
|
|
3497
|
+
return await runBulk(http, spec, client.mode, options, { storeId, body });
|
|
3498
|
+
}
|
|
3499
|
+
const allowed = await assertWriteSafety(spec, client.mode, options);
|
|
3500
|
+
if (!allowed) return 2;
|
|
3396
3501
|
const opBody = await invokeOp(http, options.resource, options.verb, {
|
|
3397
3502
|
id: options.id,
|
|
3398
|
-
storeId
|
|
3503
|
+
storeId,
|
|
3399
3504
|
parentId: options.parentId,
|
|
3400
3505
|
body
|
|
3401
3506
|
});
|
|
@@ -3453,22 +3558,74 @@ var writeCatalog = (asJson) => {
|
|
|
3453
3558
|
"\nCatalog resources (product, variant, price, file, store, \u2026) are read-only in the API.\n"
|
|
3454
3559
|
);
|
|
3455
3560
|
};
|
|
3456
|
-
var
|
|
3561
|
+
var validateBulkFlags = (spec, options) => {
|
|
3562
|
+
const bulk = Boolean(options.all || options.ids);
|
|
3563
|
+
if (!bulk) {
|
|
3564
|
+
if (options.match) return "--match needs --all.";
|
|
3565
|
+
if (options.dryRun) return "--dry-run needs --all or --ids.";
|
|
3566
|
+
return void 0;
|
|
3567
|
+
}
|
|
3568
|
+
if (!supportsBulk(spec)) {
|
|
3569
|
+
return `--all/--ids only apply to delete, cancel, and refund ops (not ${spec.verb} ${spec.resource}).`;
|
|
3570
|
+
}
|
|
3571
|
+
if (options.all && options.ids) return "Use either --all or --ids, not both.";
|
|
3572
|
+
if (options.id) return "--id targets one resource; drop it when using --all or --ids.";
|
|
3573
|
+
if (options.match && !options.all) return "--match needs --all.";
|
|
3574
|
+
return void 0;
|
|
3575
|
+
};
|
|
3576
|
+
var runBulk = async (http, spec, mode, options, scope) => {
|
|
3577
|
+
const selector = {
|
|
3578
|
+
all: options.all,
|
|
3579
|
+
ids: options.ids,
|
|
3580
|
+
match: options.match,
|
|
3581
|
+
storeId: scope.storeId,
|
|
3582
|
+
parentId: options.parentId
|
|
3583
|
+
};
|
|
3584
|
+
const targets = await collectBulkTargets(http, spec, selector);
|
|
3585
|
+
if (!options.dryRun && targets.length > 0) {
|
|
3586
|
+
const allowed = await assertWriteSafety(spec, mode, options, {
|
|
3587
|
+
count: targets.length,
|
|
3588
|
+
preview: previewTargets(targets)
|
|
3589
|
+
});
|
|
3590
|
+
if (!allowed) return 2;
|
|
3591
|
+
}
|
|
3592
|
+
const summary = await executeBulk(http, spec, targets, {
|
|
3593
|
+
dryRun: options.dryRun,
|
|
3594
|
+
body: scope.body
|
|
3595
|
+
});
|
|
3596
|
+
if (options.json) {
|
|
3597
|
+
const envelope = {
|
|
3598
|
+
ok: summary.failed === 0,
|
|
3599
|
+
mode,
|
|
3600
|
+
resource: spec.resource,
|
|
3601
|
+
verb: spec.verb,
|
|
3602
|
+
docs: `https://docs.lemonsqueezy.com/api/${spec.docsPath}`,
|
|
3603
|
+
...summary
|
|
3604
|
+
};
|
|
3605
|
+
process.stdout.write(`${JSON.stringify(envelope, null, 2)}
|
|
3606
|
+
`);
|
|
3607
|
+
} else {
|
|
3608
|
+
process.stdout.write(`${spec.verb} ${spec.resource} (mode=${mode})
|
|
3609
|
+
`);
|
|
3610
|
+
process.stdout.write(renderBulkSummary(spec, summary));
|
|
3611
|
+
}
|
|
3612
|
+
return summary.failed > 0 ? 1 : 0;
|
|
3613
|
+
};
|
|
3614
|
+
var assertWriteSafety = async (spec, mode, options, bulk) => {
|
|
3457
3615
|
const isWrite = spec.verb !== "get" && spec.verb !== "list" && spec.verb !== "current-usage";
|
|
3458
3616
|
if (!isWrite) return true;
|
|
3459
3617
|
const needsGate = Boolean(spec.destructive) || mode === "live";
|
|
3460
3618
|
if (!needsGate) return true;
|
|
3461
3619
|
if (options.yes) return true;
|
|
3462
3620
|
if (options.isInteractive) {
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
);
|
|
3621
|
+
const what = bulk ? `${spec.verb} ${bulk.count} ${spec.resource} item(s) (${bulk.preview})` : `${spec.verb} ${spec.resource}`;
|
|
3622
|
+
return confirmResourceOp(mode === "live" ? `LIVE mode: allow ${what}?` : `Confirm ${what}?`);
|
|
3466
3623
|
}
|
|
3467
3624
|
process.stderr.write(
|
|
3468
3625
|
renderCliError(
|
|
3469
3626
|
`${spec.verb} ${spec.resource} requires --yes in non-interactive ${mode} mode${spec.destructive ? " (destructive op)" : ""}`,
|
|
3470
3627
|
[
|
|
3471
|
-
`fresh-squeezy ${spec.verb} ${spec.resource} --yes \u2026`,
|
|
3628
|
+
`fresh-squeezy ${spec.verb} ${spec.resource}${bulk ? " --all" : ""} --yes \u2026`,
|
|
3472
3629
|
"Use test mode for safer experimentation: --mode test"
|
|
3473
3630
|
]
|
|
3474
3631
|
)
|
|
@@ -3840,6 +3997,28 @@ program.command("ops").description("List docs-backed resource ops (Lemon Squeezy
|
|
|
3840
3997
|
})
|
|
3841
3998
|
);
|
|
3842
3999
|
});
|
|
4000
|
+
var BULK_VERBS = ["delete", "cancel", "refund"];
|
|
4001
|
+
var opHelpText = (verb) => {
|
|
4002
|
+
if (!BULK_VERBS.includes(verb)) {
|
|
4003
|
+
return `
|
|
4004
|
+
Example:
|
|
4005
|
+
fresh-squeezy ${verb} <resource> --id 42 --json
|
|
4006
|
+
`;
|
|
4007
|
+
}
|
|
4008
|
+
const r = verb === "delete" ? "discount" : verb === "cancel" ? "subscription" : "order";
|
|
4009
|
+
return `
|
|
4010
|
+
Examples:
|
|
4011
|
+
fresh-squeezy ${verb} ${r} --id 42 --yes
|
|
4012
|
+
fresh-squeezy ${verb} ${r} --ids 1,2,3 --yes
|
|
4013
|
+
fresh-squeezy ${verb} ${r} --all --store-ids 12 --dry-run
|
|
4014
|
+
fresh-squeezy ${verb} ${r} --all --store-ids 12 --match TEST --yes --json
|
|
4015
|
+
|
|
4016
|
+
Bulk notes:
|
|
4017
|
+
--all lists every page first, then asks once (TTY) or needs --yes (non-TTY).
|
|
4018
|
+
Store falls back to LEMON_SQUEEZY_STORE_ID when --store-ids is omitted.
|
|
4019
|
+
Exit codes: 0 ok, 1 some items failed, 2 usage error / declined.
|
|
4020
|
+
`;
|
|
4021
|
+
};
|
|
3843
4022
|
for (const verb of OP_VERBS) {
|
|
3844
4023
|
program.command(`${verb} <resource>`).description(`Docs-backed ${verb} for a Lemon Squeezy resource`).option("-m, --mode <mode>", "test or live", parseMode).option("--id <id>", "Resource id").option(
|
|
3845
4024
|
"--store-ids <ids>",
|
|
@@ -3848,7 +4027,7 @@ for (const verb of OP_VERBS) {
|
|
|
3848
4027
|
).option(
|
|
3849
4028
|
"--parent-id <id>",
|
|
3850
4029
|
"Parent resource id for nested lists (product, order, subscription, \u2026)"
|
|
3851
|
-
).option("--body <json>", "JSON:API body string").option("--body-file <path>", "Path to JSON:API body file").option("--yes", "Skip confirm; required for live writes and destructive ops when non-TTY").option("--json", "Emit machine-readable JSON").action(async (resource, opts) => {
|
|
4030
|
+
).option("--body <json>", "JSON:API body string").option("--body-file <path>", "Path to JSON:API body file").option("--yes", "Skip confirm; required for live writes and destructive ops when non-TTY").option("--json", "Emit machine-readable JSON").option("--all", "Bulk: apply to every item from `list` (delete, cancel, refund)").option("--ids <ids>", "Bulk: comma-separated ids (delete, cancel, refund)", parseCsv).option("--match <text>", "Bulk: with --all, only items whose name/code/url contain <text>").option("--dry-run", "Bulk: show what would be affected; send nothing").addHelpText("after", opHelpText(verb)).action(async (resource, opts) => {
|
|
3852
4031
|
await exitWith(
|
|
3853
4032
|
runResourceOpCommand({
|
|
3854
4033
|
verb,
|
|
@@ -3861,7 +4040,11 @@ for (const verb of OP_VERBS) {
|
|
|
3861
4040
|
bodyFile: opts.bodyFile,
|
|
3862
4041
|
yes: Boolean(opts.yes),
|
|
3863
4042
|
json: Boolean(opts.json),
|
|
3864
|
-
isInteractive
|
|
4043
|
+
isInteractive,
|
|
4044
|
+
all: Boolean(opts.all),
|
|
4045
|
+
ids: opts.ids,
|
|
4046
|
+
match: opts.match,
|
|
4047
|
+
dryRun: Boolean(opts.dryRun)
|
|
3865
4048
|
})
|
|
3866
4049
|
);
|
|
3867
4050
|
});
|