ckweb-cli 0.2.4 → 0.5.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/README.md +22 -8
- package/dist/index.js +679 -9
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -172,6 +172,16 @@ function getConfigPath() {
|
|
|
172
172
|
|
|
173
173
|
// src/lib/api-client.ts
|
|
174
174
|
var DEFAULT_TIMEOUT = 3e4;
|
|
175
|
+
var RECENT_ROUTES = [
|
|
176
|
+
/^\/admin\/orders\/stats$/,
|
|
177
|
+
/^\/admin\/users\/stats$/,
|
|
178
|
+
/^\/admin\/discount-groups(\/|$)/,
|
|
179
|
+
/^\/admin\/discounts\/sync-status$/
|
|
180
|
+
];
|
|
181
|
+
function isRecentRoute(path2) {
|
|
182
|
+
const normalized = path2.startsWith("/") ? path2 : `/${path2}`;
|
|
183
|
+
return RECENT_ROUTES.some((re) => re.test(normalized));
|
|
184
|
+
}
|
|
175
185
|
async function fetchApi(method, path2, options = {}) {
|
|
176
186
|
const { body, params, timeout = DEFAULT_TIMEOUT, noAuth = false, adminAuth = false } = options;
|
|
177
187
|
let apiKey;
|
|
@@ -197,7 +207,7 @@ async function fetchApi(method, path2, options = {}) {
|
|
|
197
207
|
}
|
|
198
208
|
const headers = {
|
|
199
209
|
"Content-Type": "application/json",
|
|
200
|
-
"User-Agent": `ckweb-cli/${"0.
|
|
210
|
+
"User-Agent": `ckweb-cli/${"0.5.0"}`
|
|
201
211
|
};
|
|
202
212
|
if (apiKey) {
|
|
203
213
|
headers["x-api-key"] = apiKey;
|
|
@@ -226,6 +236,13 @@ async function fetchApi(method, path2, options = {}) {
|
|
|
226
236
|
if (response.status === 401 || response.status === 403) {
|
|
227
237
|
throw new AuthError(message);
|
|
228
238
|
}
|
|
239
|
+
if (response.status === 404 && isRecentRoute(path2)) {
|
|
240
|
+
throw new ApiError(
|
|
241
|
+
`${message} \u2014 this command may require a newer claudekit-web release; the endpoint ${path2} is not yet deployed.`,
|
|
242
|
+
response.status,
|
|
243
|
+
code
|
|
244
|
+
);
|
|
245
|
+
}
|
|
229
246
|
throw new ApiError(message, response.status, code);
|
|
230
247
|
}
|
|
231
248
|
return data;
|
|
@@ -1209,6 +1226,22 @@ function registerAdminOrdersCommand(admin) {
|
|
|
1209
1226
|
handleError(err);
|
|
1210
1227
|
}
|
|
1211
1228
|
});
|
|
1229
|
+
orders.command("search <query>").description("Search orders by query (id, user email, name)").option("--provider <name>", "Filter by payment provider").option("--limit <n>", "Number of results", "50").option("--offset <n>", "Offset for pagination", "0").action(async function(query) {
|
|
1230
|
+
try {
|
|
1231
|
+
ensureAdminAuth();
|
|
1232
|
+
const opts = this.opts();
|
|
1233
|
+
const limit = Number(opts.limit);
|
|
1234
|
+
const offset = Number(opts.offset);
|
|
1235
|
+
if (Number.isNaN(limit) || limit < 1) throw new CliError("--limit must be a positive integer");
|
|
1236
|
+
if (Number.isNaN(offset) || offset < 0) throw new CliError("--offset must be a non-negative integer");
|
|
1237
|
+
const params = { search: query, limit: String(limit), offset: String(offset) };
|
|
1238
|
+
if (opts.provider) params["provider"] = opts.provider;
|
|
1239
|
+
const data = await fetchApi("GET", "/admin/orders", { adminAuth: true, params });
|
|
1240
|
+
formatOutput(data, getOutputOpts(this), ORDER_COLUMNS2);
|
|
1241
|
+
} catch (err) {
|
|
1242
|
+
handleError(err);
|
|
1243
|
+
}
|
|
1244
|
+
});
|
|
1212
1245
|
orders.command("get <id>").description("Get order details").action(async function(id) {
|
|
1213
1246
|
try {
|
|
1214
1247
|
ensureAdminAuth();
|
|
@@ -1304,6 +1337,28 @@ function registerAdminOrdersCommand(admin) {
|
|
|
1304
1337
|
handleError(err);
|
|
1305
1338
|
}
|
|
1306
1339
|
});
|
|
1340
|
+
orders.command("resend-github-invite <id>").description("Resend GitHub repo invite for a completed order").option("--variant <type>", "Override product variant (engineer_kit|marketing_kit|combo)").option("--github-username <name>", "Override GitHub username to invite").action(async function(id) {
|
|
1341
|
+
try {
|
|
1342
|
+
ensureAdminAuth();
|
|
1343
|
+
validateId(id, "order ID");
|
|
1344
|
+
const opts = this.opts();
|
|
1345
|
+
if (opts.variant && !["engineer_kit", "marketing_kit", "combo"].includes(opts.variant)) {
|
|
1346
|
+
throw new CliError(`--variant must be one of engineer_kit, marketing_kit, combo`);
|
|
1347
|
+
}
|
|
1348
|
+
const body = {};
|
|
1349
|
+
if (opts.variant) body["variant"] = opts.variant;
|
|
1350
|
+
if (opts.githubUsername) body["githubUsername"] = opts.githubUsername;
|
|
1351
|
+
const data = await fetchApi(
|
|
1352
|
+
"POST",
|
|
1353
|
+
`/admin/orders/${id}/resend-github-invite`,
|
|
1354
|
+
{ adminAuth: true, body }
|
|
1355
|
+
);
|
|
1356
|
+
printSuccess(`GitHub invite resent for order ${id}.`);
|
|
1357
|
+
formatOutput(data, getOutputOpts(this));
|
|
1358
|
+
} catch (err) {
|
|
1359
|
+
handleError(err);
|
|
1360
|
+
}
|
|
1361
|
+
});
|
|
1307
1362
|
orders.command("export").description("Export orders as CSV").option("--start <date>", "Start date (ISO 8601)").option("--end <date>", "End date (ISO 8601)").option("--search <text>", "Search text").option("--provider <name>", "Filter by payment provider").option("--status <status>", "Filter by status").action(async function() {
|
|
1308
1363
|
try {
|
|
1309
1364
|
ensureAdminAuth();
|
|
@@ -1320,6 +1375,25 @@ function registerAdminOrdersCommand(admin) {
|
|
|
1320
1375
|
handleError(err);
|
|
1321
1376
|
}
|
|
1322
1377
|
});
|
|
1378
|
+
orders.command("stats").description("Aggregate counts + revenue with optional filters").option("--start <date>", "Start date (ISO 8601)").option("--end <date>", "End date (ISO 8601)").option("--status <status>", "Order status filter").option("--provider <polar|sepay>", "Payment provider filter").option("--vat-requested", "Only count orders with VAT invoice requested").action(async function() {
|
|
1379
|
+
try {
|
|
1380
|
+
ensureAdminAuth();
|
|
1381
|
+
const opts = this.opts();
|
|
1382
|
+
if (opts.provider && opts.provider !== "polar" && opts.provider !== "sepay") {
|
|
1383
|
+
throw new CliError("--provider must be 'polar' or 'sepay'");
|
|
1384
|
+
}
|
|
1385
|
+
const params = {};
|
|
1386
|
+
if (opts.start) params["start"] = opts.start;
|
|
1387
|
+
if (opts.end) params["end"] = opts.end;
|
|
1388
|
+
if (opts.status) params["status"] = opts.status;
|
|
1389
|
+
if (opts.provider) params["provider"] = opts.provider;
|
|
1390
|
+
if (opts.vatRequested) params["vatRequested"] = "true";
|
|
1391
|
+
const data = await fetchApi("GET", "/admin/orders/stats", { adminAuth: true, params });
|
|
1392
|
+
formatOutput(data, getOutputOpts(this));
|
|
1393
|
+
} catch (err) {
|
|
1394
|
+
handleError(err);
|
|
1395
|
+
}
|
|
1396
|
+
});
|
|
1323
1397
|
}
|
|
1324
1398
|
|
|
1325
1399
|
// src/commands/admin/admin-users.ts
|
|
@@ -1335,6 +1409,24 @@ function registerAdminUsersCommand(admin) {
|
|
|
1335
1409
|
handleError(err);
|
|
1336
1410
|
}
|
|
1337
1411
|
});
|
|
1412
|
+
users.command("update <id>").description("Update user name and/or email").option("--email <email>", "New email address").option("--name <name>", "New display name").action(async function(id) {
|
|
1413
|
+
try {
|
|
1414
|
+
ensureAdminAuth();
|
|
1415
|
+
validateId(id, "user ID");
|
|
1416
|
+
const opts = this.opts();
|
|
1417
|
+
if (opts.email === void 0 && opts.name === void 0) {
|
|
1418
|
+
throw new CliError("At least one of --email or --name is required.");
|
|
1419
|
+
}
|
|
1420
|
+
const body = {};
|
|
1421
|
+
if (opts.email !== void 0) body["email"] = opts.email;
|
|
1422
|
+
if (opts.name !== void 0) body["name"] = opts.name;
|
|
1423
|
+
const data = await fetchApi("PATCH", `/admin/users/${id}`, { adminAuth: true, body });
|
|
1424
|
+
printSuccess(`User ${id} updated.`);
|
|
1425
|
+
formatOutput(data, getOutputOpts(this));
|
|
1426
|
+
} catch (err) {
|
|
1427
|
+
handleError(err);
|
|
1428
|
+
}
|
|
1429
|
+
});
|
|
1338
1430
|
users.command("update-github <id>").description("Update GitHub username for a user").requiredOption("--username <name>", "New GitHub username").action(async function(id) {
|
|
1339
1431
|
try {
|
|
1340
1432
|
ensureAdminAuth();
|
|
@@ -1405,6 +1497,19 @@ function registerAdminUsersCommand(admin) {
|
|
|
1405
1497
|
handleError(err);
|
|
1406
1498
|
}
|
|
1407
1499
|
});
|
|
1500
|
+
users.command("stats").description("Aggregate user metrics: signups, paying users, top spenders, tiers, GitHub-linked, referrers").option("--start <date>", "Start date (ISO 8601)").option("--end <date>", "End date (ISO 8601)").action(async function() {
|
|
1501
|
+
try {
|
|
1502
|
+
ensureAdminAuth();
|
|
1503
|
+
const opts = this.opts();
|
|
1504
|
+
const params = {};
|
|
1505
|
+
if (opts.start) params["start"] = opts.start;
|
|
1506
|
+
if (opts.end) params["end"] = opts.end;
|
|
1507
|
+
const data = await fetchApi("GET", "/admin/users/stats", { adminAuth: true, params });
|
|
1508
|
+
formatOutput(data, getOutputOpts(this));
|
|
1509
|
+
} catch (err) {
|
|
1510
|
+
handleError(err);
|
|
1511
|
+
}
|
|
1512
|
+
});
|
|
1408
1513
|
}
|
|
1409
1514
|
|
|
1410
1515
|
// src/commands/admin/admin-referrals.ts
|
|
@@ -1423,6 +1528,24 @@ function registerAdminReferralsCommand(admin) {
|
|
|
1423
1528
|
handleError(err);
|
|
1424
1529
|
}
|
|
1425
1530
|
});
|
|
1531
|
+
referrals.command("user-stats <userId>").description("Per-referrer detailed stats (profile + KPIs + payouts)").option("--start <date>", "Start date (ISO 8601)").option("--end <date>", "End date (ISO 8601)").action(async function(userId) {
|
|
1532
|
+
try {
|
|
1533
|
+
ensureAdminAuth();
|
|
1534
|
+
validateId(userId, "user ID");
|
|
1535
|
+
const opts = this.opts();
|
|
1536
|
+
const params = {};
|
|
1537
|
+
if (opts.start) params["startDate"] = opts.start;
|
|
1538
|
+
if (opts.end) params["endDate"] = opts.end;
|
|
1539
|
+
const data = await fetchApi(
|
|
1540
|
+
"GET",
|
|
1541
|
+
`/admin/referrals/${userId}/stats`,
|
|
1542
|
+
{ adminAuth: true, params }
|
|
1543
|
+
);
|
|
1544
|
+
formatOutput(data, getOutputOpts(this));
|
|
1545
|
+
} catch (err) {
|
|
1546
|
+
handleError(err);
|
|
1547
|
+
}
|
|
1548
|
+
});
|
|
1426
1549
|
referrals.command("tiers-stats").description("Get referral tier statistics").action(async function() {
|
|
1427
1550
|
try {
|
|
1428
1551
|
ensureAdminAuth();
|
|
@@ -1459,13 +1582,20 @@ function registerAdminReferralsCommand(admin) {
|
|
|
1459
1582
|
// src/commands/admin/admin-revenue.ts
|
|
1460
1583
|
function registerAdminRevenueCommand(admin) {
|
|
1461
1584
|
const revenue = admin.command("revenue").description("Admin revenue reporting");
|
|
1462
|
-
revenue.command("stats").description("Get revenue statistics").option("--start <date>", "Start date (ISO 8601)").option("--end <date>", "End date (ISO 8601)").action(async function() {
|
|
1585
|
+
revenue.command("stats").description("Get revenue statistics").option("--start <date>", "Start date (ISO 8601)").option("--end <date>", "End date (ISO 8601)").option("--product <engineer_kit|marketing_kit|combo>", "Filter by product").option("--no-team", "Exclude team_* product variants (includeTeam=false)").action(async function() {
|
|
1463
1586
|
try {
|
|
1464
1587
|
ensureAdminAuth();
|
|
1465
1588
|
const opts = this.opts();
|
|
1466
1589
|
const params = {};
|
|
1467
1590
|
if (opts.start) params["startDate"] = opts.start;
|
|
1468
1591
|
if (opts.end) params["endDate"] = opts.end;
|
|
1592
|
+
if (opts.product) {
|
|
1593
|
+
if (!["engineer_kit", "marketing_kit", "combo"].includes(opts.product)) {
|
|
1594
|
+
throw new CliError("--product must be one of: engineer_kit, marketing_kit, combo");
|
|
1595
|
+
}
|
|
1596
|
+
params["product"] = opts.product;
|
|
1597
|
+
}
|
|
1598
|
+
if (opts.team === false) params["includeTeam"] = "false";
|
|
1469
1599
|
const data = await fetchApi("GET", "/admin/revenue", { adminAuth: true, params });
|
|
1470
1600
|
formatOutput(data, getOutputOpts(this));
|
|
1471
1601
|
} catch (err) {
|
|
@@ -1590,10 +1720,16 @@ var PAYOUT_COLUMNS2 = [
|
|
|
1590
1720
|
];
|
|
1591
1721
|
function registerAdminPayoutsCommand(admin) {
|
|
1592
1722
|
const payouts = admin.command("payouts").description("Admin payout management");
|
|
1593
|
-
payouts.command("list").description("List all payout requests").action(async function() {
|
|
1723
|
+
payouts.command("list").description("List all payout requests").option("--search <text>", "Search by user email").option("--status <status>", "Filter by status").option("--limit <n>", "Page size (default 50, max 100)").option("--offset <n>", "Offset for pagination (default 0)").action(async function() {
|
|
1594
1724
|
try {
|
|
1595
1725
|
ensureAdminAuth();
|
|
1596
|
-
const
|
|
1726
|
+
const opts = this.opts();
|
|
1727
|
+
const params = {};
|
|
1728
|
+
if (opts.search) params["search"] = opts.search;
|
|
1729
|
+
if (opts.status) params["status"] = opts.status;
|
|
1730
|
+
if (opts.limit !== void 0) params["limit"] = String(opts.limit);
|
|
1731
|
+
if (opts.offset !== void 0) params["offset"] = String(opts.offset);
|
|
1732
|
+
const data = await fetchApi("GET", "/admin/payout-requests", { adminAuth: true, params });
|
|
1597
1733
|
formatOutput(data, getOutputOpts(this), PAYOUT_COLUMNS2);
|
|
1598
1734
|
} catch (err) {
|
|
1599
1735
|
handleError(err);
|
|
@@ -1714,6 +1850,267 @@ function registerAdminLoyaltyCommand(admin) {
|
|
|
1714
1850
|
|
|
1715
1851
|
// src/commands/admin/admin-blog.ts
|
|
1716
1852
|
import { readFileSync as readFileSync3 } from "fs";
|
|
1853
|
+
|
|
1854
|
+
// src/commands/admin/admin-blog-categories.ts
|
|
1855
|
+
var CATEGORY_COLUMNS = [
|
|
1856
|
+
{ key: "id", header: "ID", width: 36 },
|
|
1857
|
+
{ key: "name", header: "Name", width: 24 },
|
|
1858
|
+
{ key: "slug", header: "Slug", width: 24 },
|
|
1859
|
+
{ key: "parentId", header: "Parent", width: 36 }
|
|
1860
|
+
];
|
|
1861
|
+
function registerAdminBlogCategoriesCommand(blog) {
|
|
1862
|
+
const categories = blog.command("categories").description("Manage blog categories");
|
|
1863
|
+
categories.command("list").description("List blog categories").option("--query <text>", "Filter by name or slug").action(async function() {
|
|
1864
|
+
try {
|
|
1865
|
+
ensureAdminAuth();
|
|
1866
|
+
const opts = this.opts();
|
|
1867
|
+
const params = {};
|
|
1868
|
+
if (opts.query) params["query"] = opts.query;
|
|
1869
|
+
const data = await fetchApi("GET", "/admin/blog/categories", {
|
|
1870
|
+
adminAuth: true,
|
|
1871
|
+
params
|
|
1872
|
+
});
|
|
1873
|
+
formatOutput(data.items, getOutputOpts(this), CATEGORY_COLUMNS);
|
|
1874
|
+
} catch (err) {
|
|
1875
|
+
handleError(err);
|
|
1876
|
+
}
|
|
1877
|
+
});
|
|
1878
|
+
categories.command("get <id>").description("Get a blog category by ID").action(async function(id) {
|
|
1879
|
+
try {
|
|
1880
|
+
ensureAdminAuth();
|
|
1881
|
+
validateId(id, "category ID");
|
|
1882
|
+
const data = await fetchApi("GET", `/admin/blog/categories/${id}`, {
|
|
1883
|
+
adminAuth: true
|
|
1884
|
+
});
|
|
1885
|
+
formatOutput(data.category, getOutputOpts(this));
|
|
1886
|
+
} catch (err) {
|
|
1887
|
+
handleError(err);
|
|
1888
|
+
}
|
|
1889
|
+
});
|
|
1890
|
+
categories.command("create").description("Create a blog category").requiredOption("--name <name>", "Category name").option("--slug <slug>", "Slug (auto-derived from name if omitted)").option("--description <text>", "Description").option("--parent <id>", "Parent category ID").action(async function() {
|
|
1891
|
+
try {
|
|
1892
|
+
ensureAdminAuth();
|
|
1893
|
+
const opts = this.opts();
|
|
1894
|
+
const body = { name: opts.name };
|
|
1895
|
+
if (opts.slug) body["slug"] = opts.slug;
|
|
1896
|
+
if (opts.description) body["description"] = opts.description;
|
|
1897
|
+
if (opts.parent) body["parentId"] = opts.parent;
|
|
1898
|
+
const data = await fetchApi("POST", "/admin/blog/categories", {
|
|
1899
|
+
adminAuth: true,
|
|
1900
|
+
body
|
|
1901
|
+
});
|
|
1902
|
+
printSuccess(`Category "${data.category.name}" created (${data.category.id}).`);
|
|
1903
|
+
formatOutput(data.category, getOutputOpts(this));
|
|
1904
|
+
} catch (err) {
|
|
1905
|
+
handleError(err);
|
|
1906
|
+
}
|
|
1907
|
+
});
|
|
1908
|
+
categories.command("update <id>").description("Update a blog category").option("--name <name>", "New name").option("--slug <slug>", "New slug").option("--description <text>", "New description").option("--parent <id>", "New parent category ID ('none' to clear)").action(async function(id) {
|
|
1909
|
+
try {
|
|
1910
|
+
ensureAdminAuth();
|
|
1911
|
+
validateId(id, "category ID");
|
|
1912
|
+
const opts = this.opts();
|
|
1913
|
+
const body = {};
|
|
1914
|
+
if (opts.name !== void 0) body["name"] = opts.name;
|
|
1915
|
+
if (opts.slug !== void 0) body["slug"] = opts.slug;
|
|
1916
|
+
if (opts.description !== void 0) body["description"] = opts.description;
|
|
1917
|
+
if (opts.parent !== void 0) body["parentId"] = opts.parent === "none" ? null : opts.parent;
|
|
1918
|
+
if (Object.keys(body).length === 0) {
|
|
1919
|
+
throw new CliError("At least one field (--name, --slug, --description, --parent) is required.");
|
|
1920
|
+
}
|
|
1921
|
+
const data = await fetchApi("PATCH", `/admin/blog/categories/${id}`, {
|
|
1922
|
+
adminAuth: true,
|
|
1923
|
+
body
|
|
1924
|
+
});
|
|
1925
|
+
printSuccess(`Category ${id} updated.`);
|
|
1926
|
+
formatOutput(data.category, getOutputOpts(this));
|
|
1927
|
+
} catch (err) {
|
|
1928
|
+
handleError(err);
|
|
1929
|
+
}
|
|
1930
|
+
});
|
|
1931
|
+
categories.command("delete <id>").description("Delete a blog category (refuses if articles or child categories reference it)").option("--force", "Disassociate articles before deleting (still refuses if children exist)").action(async function(id) {
|
|
1932
|
+
try {
|
|
1933
|
+
ensureAdminAuth();
|
|
1934
|
+
validateId(id, "category ID");
|
|
1935
|
+
const opts = this.opts();
|
|
1936
|
+
const ok = await confirm(`Delete category ${id}${opts.force ? " (force)" : ""}? This cannot be undone.`);
|
|
1937
|
+
if (!ok) {
|
|
1938
|
+
console.log("Aborted.");
|
|
1939
|
+
return;
|
|
1940
|
+
}
|
|
1941
|
+
const params = {};
|
|
1942
|
+
if (opts.force) params["force"] = "true";
|
|
1943
|
+
const data = await fetchApi("DELETE", `/admin/blog/categories/${id}`, {
|
|
1944
|
+
adminAuth: true,
|
|
1945
|
+
params
|
|
1946
|
+
});
|
|
1947
|
+
printSuccess(`Category ${id} deleted.`);
|
|
1948
|
+
formatOutput(data, getOutputOpts(this));
|
|
1949
|
+
} catch (err) {
|
|
1950
|
+
handleError(err);
|
|
1951
|
+
}
|
|
1952
|
+
});
|
|
1953
|
+
const articleCats = blog.command("article-categories").description("Manage article-category associations");
|
|
1954
|
+
articleCats.command("add").description("Attach a category to an article").requiredOption("--article <id>", "Article ID").requiredOption("--category <id>", "Category ID").action(async function() {
|
|
1955
|
+
try {
|
|
1956
|
+
ensureAdminAuth();
|
|
1957
|
+
const opts = this.opts();
|
|
1958
|
+
validateId(opts.article, "article ID");
|
|
1959
|
+
validateId(opts.category, "category ID");
|
|
1960
|
+
const data = await fetchApi(
|
|
1961
|
+
"POST",
|
|
1962
|
+
`/admin/blog/articles/${opts.article}/categories/${opts.category}`,
|
|
1963
|
+
{ adminAuth: true }
|
|
1964
|
+
);
|
|
1965
|
+
if (data.alreadyExisted) {
|
|
1966
|
+
console.log("Association already exists.");
|
|
1967
|
+
} else {
|
|
1968
|
+
printSuccess(`Category ${opts.category} attached to article ${opts.article}.`);
|
|
1969
|
+
}
|
|
1970
|
+
formatOutput(data, getOutputOpts(this));
|
|
1971
|
+
} catch (err) {
|
|
1972
|
+
handleError(err);
|
|
1973
|
+
}
|
|
1974
|
+
});
|
|
1975
|
+
articleCats.command("remove").description("Detach a category from an article").requiredOption("--article <id>", "Article ID").requiredOption("--category <id>", "Category ID").action(async function() {
|
|
1976
|
+
try {
|
|
1977
|
+
ensureAdminAuth();
|
|
1978
|
+
const opts = this.opts();
|
|
1979
|
+
validateId(opts.article, "article ID");
|
|
1980
|
+
validateId(opts.category, "category ID");
|
|
1981
|
+
const data = await fetchApi(
|
|
1982
|
+
"DELETE",
|
|
1983
|
+
`/admin/blog/articles/${opts.article}/categories/${opts.category}`,
|
|
1984
|
+
{ adminAuth: true }
|
|
1985
|
+
);
|
|
1986
|
+
if (data.detached) {
|
|
1987
|
+
printSuccess(`Category ${opts.category} detached from article ${opts.article}.`);
|
|
1988
|
+
} else {
|
|
1989
|
+
console.log("No association existed.");
|
|
1990
|
+
}
|
|
1991
|
+
formatOutput(data, getOutputOpts(this));
|
|
1992
|
+
} catch (err) {
|
|
1993
|
+
handleError(err);
|
|
1994
|
+
}
|
|
1995
|
+
});
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
// src/commands/admin/admin-blog-tags.ts
|
|
1999
|
+
function registerAdminBlogTagsCommand(blog) {
|
|
2000
|
+
blog.command("tags").description("List blog tags").option("--query <text>", "Search by name or slug").action(async function() {
|
|
2001
|
+
try {
|
|
2002
|
+
ensureAdminAuth();
|
|
2003
|
+
const opts = this.opts();
|
|
2004
|
+
const params = {};
|
|
2005
|
+
if (opts.query) params["query"] = opts.query;
|
|
2006
|
+
const data = await fetchApi("GET", "/admin/blog/tags", { adminAuth: true, params });
|
|
2007
|
+
formatOutput(data, getOutputOpts(this));
|
|
2008
|
+
} catch (err) {
|
|
2009
|
+
handleError(err);
|
|
2010
|
+
}
|
|
2011
|
+
});
|
|
2012
|
+
blog.command("tag-get <id>").description("Get a blog tag by id").action(async function(id) {
|
|
2013
|
+
try {
|
|
2014
|
+
ensureAdminAuth();
|
|
2015
|
+
validateId(id, "tag ID");
|
|
2016
|
+
const data = await fetchApi("GET", `/admin/blog/tags/${id}`, { adminAuth: true });
|
|
2017
|
+
formatOutput(data, getOutputOpts(this));
|
|
2018
|
+
} catch (err) {
|
|
2019
|
+
handleError(err);
|
|
2020
|
+
}
|
|
2021
|
+
});
|
|
2022
|
+
blog.command("tag-create").description("Create a blog tag").requiredOption("--name <name>", "Tag name").option("--slug <slug>", "Tag slug (auto-generated if omitted)").option("--description <text>", "Tag description").action(async function() {
|
|
2023
|
+
try {
|
|
2024
|
+
ensureAdminAuth();
|
|
2025
|
+
const opts = this.opts();
|
|
2026
|
+
const body = { name: opts.name };
|
|
2027
|
+
if (opts.slug) body["slug"] = opts.slug;
|
|
2028
|
+
if (opts.description) body["description"] = opts.description;
|
|
2029
|
+
const data = await fetchApi("POST", "/admin/blog/tags", {
|
|
2030
|
+
adminAuth: true,
|
|
2031
|
+
body
|
|
2032
|
+
});
|
|
2033
|
+
printSuccess("Tag created.");
|
|
2034
|
+
formatOutput(data, getOutputOpts(this));
|
|
2035
|
+
} catch (err) {
|
|
2036
|
+
handleError(err);
|
|
2037
|
+
}
|
|
2038
|
+
});
|
|
2039
|
+
blog.command("tag-update <id>").description("Update a blog tag").option("--name <name>", "New name").option("--slug <slug>", "New slug").option("--description <text>", "New description").action(async function(id) {
|
|
2040
|
+
try {
|
|
2041
|
+
ensureAdminAuth();
|
|
2042
|
+
validateId(id, "tag ID");
|
|
2043
|
+
const opts = this.opts();
|
|
2044
|
+
const body = {};
|
|
2045
|
+
if (opts.name !== void 0) body["name"] = opts.name;
|
|
2046
|
+
if (opts.slug !== void 0) body["slug"] = opts.slug;
|
|
2047
|
+
if (opts.description !== void 0) body["description"] = opts.description;
|
|
2048
|
+
if (Object.keys(body).length === 0) {
|
|
2049
|
+
throw new CliError("Provide at least one of --name, --slug, --description.");
|
|
2050
|
+
}
|
|
2051
|
+
const data = await fetchApi("PATCH", `/admin/blog/tags/${id}`, {
|
|
2052
|
+
adminAuth: true,
|
|
2053
|
+
body
|
|
2054
|
+
});
|
|
2055
|
+
printSuccess(`Tag ${id} updated.`);
|
|
2056
|
+
formatOutput(data, getOutputOpts(this));
|
|
2057
|
+
} catch (err) {
|
|
2058
|
+
handleError(err);
|
|
2059
|
+
}
|
|
2060
|
+
});
|
|
2061
|
+
blog.command("tag-delete <id>").description("Delete a blog tag").option("--force", "Force delete even if articles are associated (detaches them)").action(async function(id) {
|
|
2062
|
+
try {
|
|
2063
|
+
ensureAdminAuth();
|
|
2064
|
+
validateId(id, "tag ID");
|
|
2065
|
+
const opts = this.opts();
|
|
2066
|
+
const ok = await confirm(`Delete tag ${id}${opts.force ? " (force)" : ""}? This cannot be undone.`);
|
|
2067
|
+
if (!ok) {
|
|
2068
|
+
console.log("Aborted.");
|
|
2069
|
+
return;
|
|
2070
|
+
}
|
|
2071
|
+
const params = {};
|
|
2072
|
+
if (opts.force) params["force"] = "true";
|
|
2073
|
+
await fetchApi("DELETE", `/admin/blog/tags/${id}`, { adminAuth: true, params });
|
|
2074
|
+
printSuccess(`Tag ${id} deleted.`);
|
|
2075
|
+
} catch (err) {
|
|
2076
|
+
handleError(err);
|
|
2077
|
+
}
|
|
2078
|
+
});
|
|
2079
|
+
blog.command("article-tag-add <articleId> <tagId>").description("Attach a tag to an article").action(async function(articleId, tagId) {
|
|
2080
|
+
try {
|
|
2081
|
+
ensureAdminAuth();
|
|
2082
|
+
validateId(articleId, "article ID");
|
|
2083
|
+
validateId(tagId, "tag ID");
|
|
2084
|
+
const data = await fetchApi(
|
|
2085
|
+
"POST",
|
|
2086
|
+
`/admin/blog/articles/${articleId}/tags/${tagId}`,
|
|
2087
|
+
{ adminAuth: true }
|
|
2088
|
+
);
|
|
2089
|
+
printSuccess(`Tag ${tagId} attached to article ${articleId}.`);
|
|
2090
|
+
formatOutput(data, getOutputOpts(this));
|
|
2091
|
+
} catch (err) {
|
|
2092
|
+
handleError(err);
|
|
2093
|
+
}
|
|
2094
|
+
});
|
|
2095
|
+
blog.command("article-tag-remove <articleId> <tagId>").description("Detach a tag from an article").action(async function(articleId, tagId) {
|
|
2096
|
+
try {
|
|
2097
|
+
ensureAdminAuth();
|
|
2098
|
+
validateId(articleId, "article ID");
|
|
2099
|
+
validateId(tagId, "tag ID");
|
|
2100
|
+
const data = await fetchApi(
|
|
2101
|
+
"DELETE",
|
|
2102
|
+
`/admin/blog/articles/${articleId}/tags/${tagId}`,
|
|
2103
|
+
{ adminAuth: true }
|
|
2104
|
+
);
|
|
2105
|
+
printSuccess(`Tag ${tagId} detached from article ${articleId}.`);
|
|
2106
|
+
formatOutput(data, getOutputOpts(this));
|
|
2107
|
+
} catch (err) {
|
|
2108
|
+
handleError(err);
|
|
2109
|
+
}
|
|
2110
|
+
});
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
// src/commands/admin/admin-blog.ts
|
|
1717
2114
|
function resolveContent(opts) {
|
|
1718
2115
|
if (opts.file) {
|
|
1719
2116
|
try {
|
|
@@ -1725,16 +2122,41 @@ function resolveContent(opts) {
|
|
|
1725
2122
|
}
|
|
1726
2123
|
return opts.content;
|
|
1727
2124
|
}
|
|
2125
|
+
var ARTICLE_STATUSES = ["draft", "scheduled", "published", "archived"];
|
|
2126
|
+
function applySchedulingFields(body, opts) {
|
|
2127
|
+
if (opts.status !== void 0) {
|
|
2128
|
+
if (!ARTICLE_STATUSES.includes(opts.status)) {
|
|
2129
|
+
throw new CliError(`--status must be one of: ${ARTICLE_STATUSES.join(", ")}`);
|
|
2130
|
+
}
|
|
2131
|
+
body["status"] = opts.status;
|
|
2132
|
+
}
|
|
2133
|
+
if (opts.scheduledFor !== void 0) {
|
|
2134
|
+
const d = new Date(opts.scheduledFor);
|
|
2135
|
+
if (Number.isNaN(d.getTime())) {
|
|
2136
|
+
throw new CliError(`--scheduled-for must be a valid ISO 8601 datetime (got "${opts.scheduledFor}")`);
|
|
2137
|
+
}
|
|
2138
|
+
if (opts.status && opts.status !== "scheduled" && opts.status !== "draft") {
|
|
2139
|
+
throw new CliError(
|
|
2140
|
+
`--scheduled-for cannot be combined with --status ${opts.status}; use --status scheduled (or omit --status).`
|
|
2141
|
+
);
|
|
2142
|
+
}
|
|
2143
|
+
body["scheduledFor"] = d.toISOString();
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
1728
2146
|
function registerAdminBlogCommand(admin) {
|
|
1729
2147
|
const blog = admin.command("blog").description("Admin blog management");
|
|
1730
|
-
|
|
2148
|
+
registerAdminBlogCategoriesCommand(blog);
|
|
2149
|
+
registerAdminBlogTagsCommand(blog);
|
|
2150
|
+
blog.command("create").description("Create a new blog article").requiredOption("--title <title>", "Article title").option("--content <content>", "Article content (inline)").option("--file <filepath>", "Read content from file").option("--status <draft|scheduled|published|archived>", "Article status").option("--scheduled-for <iso-date>", "Publish at this ISO 8601 datetime").action(async function() {
|
|
1731
2151
|
try {
|
|
1732
2152
|
ensureAdminAuth();
|
|
1733
2153
|
const opts = this.opts();
|
|
1734
2154
|
const content = resolveContent(opts);
|
|
2155
|
+
const body = { title: opts.title, content };
|
|
2156
|
+
applySchedulingFields(body, { status: opts.status, scheduledFor: opts.scheduledFor });
|
|
1735
2157
|
const data = await fetchApi("POST", "/blog/articles", {
|
|
1736
2158
|
adminAuth: true,
|
|
1737
|
-
body
|
|
2159
|
+
body
|
|
1738
2160
|
});
|
|
1739
2161
|
printSuccess("Article created.");
|
|
1740
2162
|
formatOutput(data, getOutputOpts(this));
|
|
@@ -1742,7 +2164,7 @@ function registerAdminBlogCommand(admin) {
|
|
|
1742
2164
|
handleError(err);
|
|
1743
2165
|
}
|
|
1744
2166
|
});
|
|
1745
|
-
blog.command("update <id>").description("Update an existing blog article").option("--title <title>", "New title").option("--content <content>", "New content (inline)").option("--file <filepath>", "Read new content from file").action(async function(id) {
|
|
2167
|
+
blog.command("update <id>").description("Update an existing blog article").option("--title <title>", "New title").option("--content <content>", "New content (inline)").option("--file <filepath>", "Read new content from file").option("--status <draft|scheduled|published|archived>", "Article status").option("--scheduled-for <iso-date>", "Publish at this ISO 8601 datetime").action(async function(id) {
|
|
1746
2168
|
try {
|
|
1747
2169
|
ensureAdminAuth();
|
|
1748
2170
|
validateId(id, "article ID");
|
|
@@ -1751,6 +2173,7 @@ function registerAdminBlogCommand(admin) {
|
|
|
1751
2173
|
const body = {};
|
|
1752
2174
|
if (opts.title) body["title"] = opts.title;
|
|
1753
2175
|
if (content !== void 0) body["content"] = content;
|
|
2176
|
+
applySchedulingFields(body, { status: opts.status, scheduledFor: opts.scheduledFor });
|
|
1754
2177
|
const data = await fetchApi("PATCH", `/blog/articles/${id}`, {
|
|
1755
2178
|
adminAuth: true,
|
|
1756
2179
|
body
|
|
@@ -1846,6 +2269,16 @@ function registerAdminBlogCommand(admin) {
|
|
|
1846
2269
|
handleError(err);
|
|
1847
2270
|
}
|
|
1848
2271
|
});
|
|
2272
|
+
blog.command("run-scheduler").description("Manually trigger the blog article scheduler").action(async function() {
|
|
2273
|
+
try {
|
|
2274
|
+
ensureAdminAuth();
|
|
2275
|
+
const data = await fetchApi("POST", "/blog/scheduler", { adminAuth: true });
|
|
2276
|
+
printSuccess("Scheduler executed.");
|
|
2277
|
+
formatOutput(data, getOutputOpts(this));
|
|
2278
|
+
} catch (err) {
|
|
2279
|
+
handleError(err);
|
|
2280
|
+
}
|
|
2281
|
+
});
|
|
1849
2282
|
blog.command("export").description("Export blog articles as CSV").option("--start <date>", "Start date (ISO 8601)").option("--end <date>", "End date (ISO 8601)").option("--status <status>", "Filter by status").action(async function() {
|
|
1850
2283
|
try {
|
|
1851
2284
|
ensureAdminAuth();
|
|
@@ -1889,6 +2322,23 @@ function registerAdminKeysCommand(admin) {
|
|
|
1889
2322
|
handleError(err);
|
|
1890
2323
|
}
|
|
1891
2324
|
});
|
|
2325
|
+
keys.command("search <query>").description("Search API keys by name or scope").option("--limit <n>", "Number of results", "50").option("--offset <n>", "Offset for pagination", "0").action(async function(query) {
|
|
2326
|
+
try {
|
|
2327
|
+
ensureAdminAuth();
|
|
2328
|
+
const opts = this.opts();
|
|
2329
|
+
const limit = Number(opts.limit);
|
|
2330
|
+
const offset = Number(opts.offset);
|
|
2331
|
+
if (Number.isNaN(limit) || limit < 1) throw new Error("--limit must be a positive number");
|
|
2332
|
+
if (Number.isNaN(offset) || offset < 0) throw new Error("--offset must be a non-negative number");
|
|
2333
|
+
const data = await fetchApi("GET", "/admin/keys", {
|
|
2334
|
+
adminAuth: true,
|
|
2335
|
+
params: { search: query, limit: String(limit), offset: String(offset) }
|
|
2336
|
+
});
|
|
2337
|
+
formatOutput(data, getOutputOpts(this), KEY_COLUMNS);
|
|
2338
|
+
} catch (err) {
|
|
2339
|
+
handleError(err);
|
|
2340
|
+
}
|
|
2341
|
+
});
|
|
1892
2342
|
keys.command("create").description("Create a new API key").requiredOption("--name <name>", "Key name").requiredOption("--scopes <scopes>", "Comma-separated list of scopes").action(async function() {
|
|
1893
2343
|
try {
|
|
1894
2344
|
ensureAdminAuth();
|
|
@@ -1978,6 +2428,23 @@ function registerAdminInvitesCommand(admin) {
|
|
|
1978
2428
|
handleError(err);
|
|
1979
2429
|
}
|
|
1980
2430
|
});
|
|
2431
|
+
invites.command("search <query>").description("Search invites by email").option("--limit <n>", "Number of results", "50").option("--offset <n>", "Offset for pagination", "0").action(async function(query) {
|
|
2432
|
+
try {
|
|
2433
|
+
ensureAdminAuth();
|
|
2434
|
+
const opts = this.opts();
|
|
2435
|
+
const limit = Number(opts.limit);
|
|
2436
|
+
const offset = Number(opts.offset);
|
|
2437
|
+
if (Number.isNaN(limit) || limit < 1) throw new CliError("--limit must be a positive integer");
|
|
2438
|
+
if (Number.isNaN(offset) || offset < 0) throw new CliError("--offset must be a non-negative integer");
|
|
2439
|
+
const data = await fetchApi("GET", "/admin/invites", {
|
|
2440
|
+
adminAuth: true,
|
|
2441
|
+
params: { search: query, limit: String(limit), offset: String(offset) }
|
|
2442
|
+
});
|
|
2443
|
+
formatOutput(data, getOutputOpts(this), INVITE_COLUMNS);
|
|
2444
|
+
} catch (err) {
|
|
2445
|
+
handleError(err);
|
|
2446
|
+
}
|
|
2447
|
+
});
|
|
1981
2448
|
invites.command("create").description("Create a new invite").requiredOption("--email <email>", "Email address to invite").action(async function() {
|
|
1982
2449
|
try {
|
|
1983
2450
|
ensureAdminAuth();
|
|
@@ -2069,6 +2536,35 @@ function registerAdminInvitesCommand(admin) {
|
|
|
2069
2536
|
});
|
|
2070
2537
|
}
|
|
2071
2538
|
|
|
2539
|
+
// src/commands/admin/admin-licenses.ts
|
|
2540
|
+
function registerAdminLicensesCommand(admin) {
|
|
2541
|
+
const licenses = admin.command("licenses").description("Admin license management");
|
|
2542
|
+
licenses.command("update-github <licenseId>").description("Change the GitHub username for a single license (admin override)").requiredOption("--username <name>", "New GitHub username").option("--skip-revoke", "Skip revoking the previous collaborator", false).action(async function(licenseId) {
|
|
2543
|
+
try {
|
|
2544
|
+
ensureAdminAuth();
|
|
2545
|
+
validateId(licenseId, "license ID");
|
|
2546
|
+
const { username, skipRevoke } = this.opts();
|
|
2547
|
+
const ok = await confirm(
|
|
2548
|
+
`Change GitHub username on license ${licenseId} to "${username}"?${skipRevoke ? " (skip revoke)" : ""}`
|
|
2549
|
+
);
|
|
2550
|
+
if (!ok) {
|
|
2551
|
+
console.log("Aborted.");
|
|
2552
|
+
return;
|
|
2553
|
+
}
|
|
2554
|
+
const body = { githubUsername: username };
|
|
2555
|
+
if (skipRevoke) body["skipRevoke"] = true;
|
|
2556
|
+
const data = await fetchApi("PATCH", `/admin/licenses/${licenseId}/github-username`, {
|
|
2557
|
+
adminAuth: true,
|
|
2558
|
+
body
|
|
2559
|
+
});
|
|
2560
|
+
printSuccess(`License ${licenseId} GitHub username updated to ${username}.`);
|
|
2561
|
+
formatOutput(data, getOutputOpts(this));
|
|
2562
|
+
} catch (err) {
|
|
2563
|
+
handleError(err);
|
|
2564
|
+
}
|
|
2565
|
+
});
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2072
2568
|
// src/commands/admin/admin-admins.ts
|
|
2073
2569
|
function registerAdminAdminsCommand(admin) {
|
|
2074
2570
|
const admins = admin.command("admins").description("Admin role management");
|
|
@@ -2083,6 +2579,23 @@ function registerAdminAdminsCommand(admin) {
|
|
|
2083
2579
|
handleError(err);
|
|
2084
2580
|
}
|
|
2085
2581
|
});
|
|
2582
|
+
admins.command("search <query>").description("Search admins by email or name").option("--limit <n>", "Number of results", "50").option("--offset <n>", "Offset for pagination", "0").action(async function(query) {
|
|
2583
|
+
try {
|
|
2584
|
+
ensureAdminAuth();
|
|
2585
|
+
const opts = this.opts();
|
|
2586
|
+
const limit = Number(opts.limit);
|
|
2587
|
+
const offset = Number(opts.offset);
|
|
2588
|
+
if (Number.isNaN(limit) || limit < 1) throw new CliError("--limit must be a positive integer");
|
|
2589
|
+
if (Number.isNaN(offset) || offset < 0) throw new CliError("--offset must be a non-negative integer");
|
|
2590
|
+
const data = await fetchApi("GET", "/admin/admins", {
|
|
2591
|
+
adminAuth: true,
|
|
2592
|
+
params: { search: query, limit: String(limit), offset: String(offset) }
|
|
2593
|
+
});
|
|
2594
|
+
formatOutput(data, getOutputOpts(this));
|
|
2595
|
+
} catch (err) {
|
|
2596
|
+
handleError(err);
|
|
2597
|
+
}
|
|
2598
|
+
});
|
|
2086
2599
|
admins.command("get <id>").description("Get admin details").action(async function(id) {
|
|
2087
2600
|
try {
|
|
2088
2601
|
ensureAdminAuth();
|
|
@@ -2212,7 +2725,7 @@ function registerAdminDiscountsCommand(admin) {
|
|
|
2212
2725
|
page: String(page),
|
|
2213
2726
|
limit: String(limit)
|
|
2214
2727
|
};
|
|
2215
|
-
if (opts.query) params["
|
|
2728
|
+
if (opts.query) params["search"] = opts.query;
|
|
2216
2729
|
const data = await fetchApi("GET", "/admin/discounts", {
|
|
2217
2730
|
adminAuth: true,
|
|
2218
2731
|
params
|
|
@@ -2222,6 +2735,25 @@ function registerAdminDiscountsCommand(admin) {
|
|
|
2222
2735
|
handleError(err);
|
|
2223
2736
|
}
|
|
2224
2737
|
});
|
|
2738
|
+
discounts.command("search <query>").description("Search discounts by code or description").option("--page <n>", "Page number", "1").option("--limit <n>", "Results per page", "50").action(async function(query) {
|
|
2739
|
+
try {
|
|
2740
|
+
ensureAdminAuth();
|
|
2741
|
+
const opts = this.opts();
|
|
2742
|
+
const page = Number(opts.page);
|
|
2743
|
+
const limit = Number(opts.limit);
|
|
2744
|
+
if (Number.isNaN(page) || page < 1) throw new CliError("--page must be a positive integer");
|
|
2745
|
+
if (Number.isNaN(limit) || limit < 1) throw new CliError("--limit must be a positive integer");
|
|
2746
|
+
const params = {
|
|
2747
|
+
search: query,
|
|
2748
|
+
page: String(page),
|
|
2749
|
+
limit: String(limit)
|
|
2750
|
+
};
|
|
2751
|
+
const data = await fetchApi("GET", "/admin/discounts", { adminAuth: true, params });
|
|
2752
|
+
formatOutput(data, getOutputOpts(this), DISCOUNT_COLUMNS);
|
|
2753
|
+
} catch (err) {
|
|
2754
|
+
handleError(err);
|
|
2755
|
+
}
|
|
2756
|
+
});
|
|
2225
2757
|
discounts.command("create").description("Create a discount code").requiredOption("--code <code>", "Discount code").requiredOption("--type <percentage|fixed>", "Discount type").option("--amount <n>", "Amount in cents (fixed type)").option("--basis-points <n>", "Basis points (percentage type)").option("--max-redemptions <n>", "Maximum number of redemptions").option("--starts <date>", "Start date (ISO 8601)").option("--ends <date>", "End date (ISO 8601)").action(async function() {
|
|
2226
2758
|
try {
|
|
2227
2759
|
ensureAdminAuth();
|
|
@@ -2272,6 +2804,15 @@ function registerAdminDiscountsCommand(admin) {
|
|
|
2272
2804
|
handleError(err);
|
|
2273
2805
|
}
|
|
2274
2806
|
});
|
|
2807
|
+
discounts.command("sync-status").description("Show Polar sync stats for all discounts").action(async function() {
|
|
2808
|
+
try {
|
|
2809
|
+
ensureAdminAuth();
|
|
2810
|
+
const data = await fetchApi("GET", "/admin/discounts/sync-status", { adminAuth: true });
|
|
2811
|
+
formatOutput(data, getOutputOpts(this));
|
|
2812
|
+
} catch (err) {
|
|
2813
|
+
handleError(err);
|
|
2814
|
+
}
|
|
2815
|
+
});
|
|
2275
2816
|
discounts.command("export").description("Export discounts as CSV").option("--start <date>", "Start date filter (ISO 8601)").option("--end <date>", "End date filter (ISO 8601)").option("--status <all|active|expired>", "Filter by status", "all").action(async function() {
|
|
2276
2817
|
try {
|
|
2277
2818
|
ensureAdminAuth();
|
|
@@ -2290,6 +2831,133 @@ function registerAdminDiscountsCommand(admin) {
|
|
|
2290
2831
|
});
|
|
2291
2832
|
}
|
|
2292
2833
|
|
|
2834
|
+
// src/commands/admin/admin-discount-groups.ts
|
|
2835
|
+
var GROUP_COLUMNS = [
|
|
2836
|
+
{ key: "id", header: "ID", width: 12 },
|
|
2837
|
+
{ key: "name", header: "Name", width: 24 },
|
|
2838
|
+
{ key: "prefix", header: "Prefix", width: 12 },
|
|
2839
|
+
{ key: "type", header: "Type", width: 12 },
|
|
2840
|
+
{ key: "codesGenerated", header: "Generated", width: 10 },
|
|
2841
|
+
{ key: "codesUsed", header: "Used", width: 8 },
|
|
2842
|
+
{ key: "createdAt", header: "Created", width: 22 }
|
|
2843
|
+
];
|
|
2844
|
+
function buildGroupBody(opts) {
|
|
2845
|
+
const body = {};
|
|
2846
|
+
if (opts.name !== void 0) body["name"] = opts.name;
|
|
2847
|
+
if (opts.prefix !== void 0) body["prefix"] = opts.prefix;
|
|
2848
|
+
if (opts.type !== void 0) {
|
|
2849
|
+
if (opts.type !== "percentage" && opts.type !== "fixed") {
|
|
2850
|
+
throw new CliError("--type must be 'percentage' or 'fixed'");
|
|
2851
|
+
}
|
|
2852
|
+
body["type"] = opts.type;
|
|
2853
|
+
}
|
|
2854
|
+
if (opts.amount !== void 0) {
|
|
2855
|
+
const v = Number(opts.amount);
|
|
2856
|
+
if (Number.isNaN(v) || v < 0) throw new CliError("--amount must be a non-negative number (cents)");
|
|
2857
|
+
body["amount"] = v;
|
|
2858
|
+
}
|
|
2859
|
+
if (opts.basisPoints !== void 0) {
|
|
2860
|
+
const v = Number(opts.basisPoints);
|
|
2861
|
+
if (Number.isNaN(v) || v < 0 || v > 1e4) throw new CliError("--basis-points must be 0..10000");
|
|
2862
|
+
body["basisPoints"] = v;
|
|
2863
|
+
}
|
|
2864
|
+
if (opts.duration !== void 0) {
|
|
2865
|
+
if (!["once", "repeating", "forever"].includes(opts.duration)) {
|
|
2866
|
+
throw new CliError("--duration must be one of: once, repeating, forever");
|
|
2867
|
+
}
|
|
2868
|
+
body["duration"] = opts.duration;
|
|
2869
|
+
}
|
|
2870
|
+
if (opts.durationInMonths !== void 0) {
|
|
2871
|
+
const v = Number(opts.durationInMonths);
|
|
2872
|
+
if (Number.isNaN(v) || v < 1 || v > 24) throw new CliError("--duration-in-months must be 1..24");
|
|
2873
|
+
body["durationInMonths"] = v;
|
|
2874
|
+
}
|
|
2875
|
+
if (opts.maxRedemptionsPerCode !== void 0) {
|
|
2876
|
+
const v = Number(opts.maxRedemptionsPerCode);
|
|
2877
|
+
if (Number.isNaN(v) || v < 1) throw new CliError("--max-redemptions-per-code must be a positive integer");
|
|
2878
|
+
body["maxRedemptionsPerCode"] = v;
|
|
2879
|
+
}
|
|
2880
|
+
if (opts.starts !== void 0) body["startsAt"] = opts.starts;
|
|
2881
|
+
if (opts.ends !== void 0) body["endsAt"] = opts.ends;
|
|
2882
|
+
return body;
|
|
2883
|
+
}
|
|
2884
|
+
function registerAdminDiscountGroupsCommand(admin) {
|
|
2885
|
+
const groups = admin.command("discount-groups").description("Discount group management");
|
|
2886
|
+
groups.command("list").description("List discount groups").option("--query <text>", "Search by name or prefix").option("--page <n>", "Page number", "1").option("--limit <n>", "Page size", "50").action(async function() {
|
|
2887
|
+
try {
|
|
2888
|
+
ensureAdminAuth();
|
|
2889
|
+
const opts = this.opts();
|
|
2890
|
+
const params = { page: String(opts.page), limit: String(opts.limit) };
|
|
2891
|
+
if (opts.query) params["query"] = opts.query;
|
|
2892
|
+
const data = await fetchApi("GET", "/admin/discount-groups", {
|
|
2893
|
+
adminAuth: true,
|
|
2894
|
+
params
|
|
2895
|
+
});
|
|
2896
|
+
formatOutput(data.items ?? data, getOutputOpts(this), GROUP_COLUMNS);
|
|
2897
|
+
} catch (err) {
|
|
2898
|
+
handleError(err);
|
|
2899
|
+
}
|
|
2900
|
+
});
|
|
2901
|
+
groups.command("get <id>").description("Get a discount group by ID").action(async function(id) {
|
|
2902
|
+
try {
|
|
2903
|
+
ensureAdminAuth();
|
|
2904
|
+
validateId(id, "discount group ID");
|
|
2905
|
+
const data = await fetchApi("GET", `/admin/discount-groups/${id}`, { adminAuth: true });
|
|
2906
|
+
formatOutput(data, getOutputOpts(this));
|
|
2907
|
+
} catch (err) {
|
|
2908
|
+
handleError(err);
|
|
2909
|
+
}
|
|
2910
|
+
});
|
|
2911
|
+
groups.command("create").description("Create a discount group template").requiredOption("--name <name>", "Group name").requiredOption("--prefix <prefix>", "Code prefix (3-20 chars, uppercased)").requiredOption("--type <percentage|fixed>", "Discount type").option("--amount <n>", "Amount in cents (fixed type)").option("--basis-points <n>", "Basis points (percentage type, 0..10000)").option("--duration <once|repeating|forever>", "Discount duration").option("--duration-in-months <n>", "Months for repeating duration (1..24)").option("--max-redemptions-per-code <n>", "Per-code redemption cap").option("--starts <date>", "Start date (ISO 8601)").option("--ends <date>", "End date (ISO 8601)").action(async function() {
|
|
2912
|
+
try {
|
|
2913
|
+
ensureAdminAuth();
|
|
2914
|
+
const body = buildGroupBody(this.opts());
|
|
2915
|
+
const data = await fetchApi("POST", "/admin/discount-groups", {
|
|
2916
|
+
adminAuth: true,
|
|
2917
|
+
body
|
|
2918
|
+
});
|
|
2919
|
+
printSuccess(`Discount group created: ${data?.group?.id ?? ""}`);
|
|
2920
|
+
formatOutput(data, getOutputOpts(this));
|
|
2921
|
+
} catch (err) {
|
|
2922
|
+
handleError(err);
|
|
2923
|
+
}
|
|
2924
|
+
});
|
|
2925
|
+
groups.command("update <id>").description("Update a discount group (all fields optional)").option("--name <name>", "Group name").option("--prefix <prefix>", "Code prefix").option("--type <percentage|fixed>", "Discount type").option("--amount <n>", "Amount in cents").option("--basis-points <n>", "Basis points").option("--duration <once|repeating|forever>", "Discount duration").option("--duration-in-months <n>", "Months for repeating").option("--max-redemptions-per-code <n>", "Per-code redemption cap").option("--starts <date>", "Start date (ISO 8601)").option("--ends <date>", "End date (ISO 8601)").action(async function(id) {
|
|
2926
|
+
try {
|
|
2927
|
+
ensureAdminAuth();
|
|
2928
|
+
validateId(id, "discount group ID");
|
|
2929
|
+
const body = buildGroupBody(this.opts());
|
|
2930
|
+
if (Object.keys(body).length === 0) {
|
|
2931
|
+
throw new CliError("At least one option must be provided to update.");
|
|
2932
|
+
}
|
|
2933
|
+
const data = await fetchApi("PATCH", `/admin/discount-groups/${id}`, {
|
|
2934
|
+
adminAuth: true,
|
|
2935
|
+
body
|
|
2936
|
+
});
|
|
2937
|
+
printSuccess(`Discount group ${id} updated.`);
|
|
2938
|
+
formatOutput(data, getOutputOpts(this));
|
|
2939
|
+
} catch (err) {
|
|
2940
|
+
handleError(err);
|
|
2941
|
+
}
|
|
2942
|
+
});
|
|
2943
|
+
groups.command("delete <id>").description("Delete a discount group (soft-deletes member codes)").action(async function(id) {
|
|
2944
|
+
try {
|
|
2945
|
+
ensureAdminAuth();
|
|
2946
|
+
validateId(id, "discount group ID");
|
|
2947
|
+
const ok = await confirm(`Delete discount group ${id}? This soft-deletes all member codes.`);
|
|
2948
|
+
if (!ok) {
|
|
2949
|
+
console.log("Aborted.");
|
|
2950
|
+
return;
|
|
2951
|
+
}
|
|
2952
|
+
const data = await fetchApi("DELETE", `/admin/discount-groups/${id}`, { adminAuth: true });
|
|
2953
|
+
printSuccess(`Discount group ${id} deleted.`);
|
|
2954
|
+
formatOutput(data, getOutputOpts(this));
|
|
2955
|
+
} catch (err) {
|
|
2956
|
+
handleError(err);
|
|
2957
|
+
}
|
|
2958
|
+
});
|
|
2959
|
+
}
|
|
2960
|
+
|
|
2293
2961
|
// src/commands/admin/admin-tiers.ts
|
|
2294
2962
|
var TIER_COLUMNS = [
|
|
2295
2963
|
{ key: "id", header: "ID", width: 12 },
|
|
@@ -2325,15 +2993,17 @@ function registerAdminCommand(program2) {
|
|
|
2325
2993
|
registerAdminBlogCommand(admin);
|
|
2326
2994
|
registerAdminKeysCommand(admin);
|
|
2327
2995
|
registerAdminInvitesCommand(admin);
|
|
2996
|
+
registerAdminLicensesCommand(admin);
|
|
2328
2997
|
registerAdminAdminsCommand(admin);
|
|
2329
2998
|
registerAdminDiscountsCommand(admin);
|
|
2999
|
+
registerAdminDiscountGroupsCommand(admin);
|
|
2330
3000
|
registerAdminTiersCommand(admin);
|
|
2331
3001
|
}
|
|
2332
3002
|
|
|
2333
3003
|
// src/index.ts
|
|
2334
3004
|
loadDotenvFiles();
|
|
2335
3005
|
var program = new Command();
|
|
2336
|
-
program.name("ckweb").description("CLI for interacting with ClaudeKit.cc API").version("0.
|
|
3006
|
+
program.name("ckweb").description("CLI for interacting with ClaudeKit.cc API").version("0.5.0");
|
|
2337
3007
|
program.option("--json", "Output as JSON").option("--table", "Output as table").option("--quiet", "Minimal output");
|
|
2338
3008
|
registerAuthCommand(program);
|
|
2339
3009
|
registerHealthCommand(program);
|