ckweb-cli 0.3.0 → 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/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.3.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 data = await fetchApi("GET", "/admin/payout-requests", { adminAuth: true });
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
- 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").action(async function() {
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: { title: opts.title, content }
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["query"] = opts.query;
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.3.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);