ckweb-cli 0.5.0 → 0.6.0-beta.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 CHANGED
@@ -110,8 +110,8 @@ ckweb seo traffic "example.com"
110
110
  ### Admin Commands (requires admin key)
111
111
  | Group | Commands |
112
112
  |-------|----------|
113
- | `admin orders` | list, search, get, complete, refund, refund-keep-access, switch-product (engineer_kit/marketing_kit), resend-emails, resend-github-invite, export, stats (--start, --end, --status, --provider, --vat-requested) |
114
- | `admin users` | get, update, update-github, update-rates, set-tier, export, stats (--start, --end) |
113
+ | `admin orders` | list, search, get, invoice (--output), complete, refund, refund-keep-access, switch-product (engineer_kit/marketing_kit), resend-emails, resend-github-invite, export, stats (--start, --end, --status, --provider, --vat-requested) |
114
+ | `admin users` | get, update, update-github, update-rates, set-tier, delete-orphan (--reason), export, stats (--start, --end) |
115
115
  | `admin referrals` | stats, user-stats, diagnose, export, tiers-stats |
116
116
  | `admin revenue` | stats (--product, --no-team), export, maintainer, costs, add-cost, update-cost, delete-cost, reports |
117
117
  | `admin payouts` | list (--search, --status, --limit, --offset), update, export, export-detailed, request-csv |
@@ -198,7 +198,7 @@ pnpm test:e2e # Run e2e tests against the real claudekit API
198
198
 
199
199
  `pnpm test:e2e` builds the CLI and exercises the published binary against the real claudekit API.
200
200
  Requires both `CKWEB_API_KEY` and `CKWEB_ADMIN_API_KEY` in `.env` (or shell env).
201
- These tests run on PRs and on `main` push, and gate the npm publish step in `release.yml`.
201
+ These tests run on PRs and on `main` push, gate the npm publish step in `release.yml`, and are duplicated inside the configured beta release workflow for `dev`.
202
202
 
203
203
  ## Releasing
204
204
 
@@ -218,6 +218,26 @@ Bump rules (`auto` mode):
218
218
  - `feat:` → minor
219
219
  - `fix:` / `perf:` → patch
220
220
  - `BREAKING CHANGE` or `feat!:` / `fix!:` → major
221
+
222
+ ### Beta releases
223
+
224
+ Beta release automation is configured in `.github/workflows/beta-release.yml` for pushes to `dev` and manual dispatch.
225
+
226
+ The beta lane:
227
+ - Runs install, lint, unit/integration tests, build, and real API e2e tests.
228
+ - Computes semver prereleases like `0.5.2-beta.0`, `0.5.2-beta.1`.
229
+ - Updates root `CHANGELOG.md`.
230
+ - Checks that the target npm version does not already exist.
231
+ - Runs `npm publish --tag beta --dry-run` before pushing the beta version commit and tag.
232
+ - Pushes the beta commit/tag atomically to `dev`, then publishes with npm dist-tag `beta`.
233
+
234
+ Install beta builds with:
235
+
236
+ ```bash
237
+ npm install -g ckweb-cli@beta
238
+ ```
239
+
240
+ Current status: configured locally in this branch, not yet verified by a real `dev` workflow run.
221
241
  - no relevant commits → skipped
222
242
 
223
243
  The workflow bumps `package.json`, creates `chore(release): vX.Y.Z` commit + tag,
package/dist/index.js CHANGED
@@ -182,8 +182,7 @@ function isRecentRoute(path2) {
182
182
  const normalized = path2.startsWith("/") ? path2 : `/${path2}`;
183
183
  return RECENT_ROUTES.some((re) => re.test(normalized));
184
184
  }
185
- async function fetchApi(method, path2, options = {}) {
186
- const { body, params, timeout = DEFAULT_TIMEOUT, noAuth = false, adminAuth = false } = options;
185
+ function resolveApiKey(noAuth, adminAuth) {
187
186
  let apiKey;
188
187
  if (!noAuth) {
189
188
  if (adminAuth) {
@@ -198,6 +197,9 @@ async function fetchApi(method, path2, options = {}) {
198
197
  }
199
198
  }
200
199
  }
200
+ return apiKey;
201
+ }
202
+ function buildUrl(path2, params) {
201
203
  const baseUrl = getBaseUrl().replace(/\/?$/, "/");
202
204
  const url = new URL(path2.replace(/^\//, ""), baseUrl);
203
205
  if (params) {
@@ -205,59 +207,109 @@ async function fetchApi(method, path2, options = {}) {
205
207
  url.searchParams.set(key, value);
206
208
  }
207
209
  }
210
+ return url;
211
+ }
212
+ function buildHeaders(apiKey, hasBody) {
208
213
  const headers = {
209
- "Content-Type": "application/json",
210
- "User-Agent": `ckweb-cli/${"0.5.0"}`
214
+ "User-Agent": `ckweb-cli/${"0.6.0-beta.0"}`
211
215
  };
216
+ if (hasBody) {
217
+ headers["Content-Type"] = "application/json";
218
+ }
212
219
  if (apiKey) {
213
220
  headers["x-api-key"] = apiKey;
214
221
  }
222
+ return headers;
223
+ }
224
+ async function parseResponseBody(response) {
225
+ const contentType = response.headers.get("content-type") || "";
226
+ if (contentType.includes("application/json")) {
227
+ return response.json();
228
+ }
229
+ return response.text();
230
+ }
231
+ function throwResponseError(response, path2, data) {
232
+ const errorBody = data && typeof data === "object" ? data : {};
233
+ const message = errorBody.error || errorBody.message || (typeof data === "string" && data.trim() ? data : `HTTP ${response.status}`);
234
+ const code = errorBody.code;
235
+ if (response.status === 401 || response.status === 403) {
236
+ throw new AuthError(message);
237
+ }
238
+ if (response.status === 404 && isRecentRoute(path2)) {
239
+ throw new ApiError(
240
+ `${message} \u2014 this command may require a newer claudekit-web release; the endpoint ${path2} is not yet deployed.`,
241
+ response.status,
242
+ code
243
+ );
244
+ }
245
+ throw new ApiError(message, response.status, code);
246
+ }
247
+ function createTimeout(timeout) {
215
248
  const controller = new AbortController();
216
249
  const timer = setTimeout(() => controller.abort(), timeout);
250
+ return { controller, timer };
251
+ }
252
+ function normalizeRequestError(err, timeout) {
253
+ if (err instanceof AuthError || err instanceof ApiError) {
254
+ throw err;
255
+ }
256
+ if (err instanceof DOMException && err.name === "AbortError") {
257
+ throw new NetworkError(`Request timed out after ${timeout / 1e3}s`);
258
+ }
259
+ if (err instanceof TypeError) {
260
+ throw new NetworkError();
261
+ }
262
+ throw err;
263
+ }
264
+ async function fetchApi(method, path2, options = {}) {
265
+ const { body, params, timeout = DEFAULT_TIMEOUT, noAuth = false, adminAuth = false } = options;
266
+ const apiKey = resolveApiKey(noAuth, adminAuth);
267
+ const url = buildUrl(path2, params);
268
+ const headers = buildHeaders(apiKey, body !== void 0);
269
+ const { controller, timer } = createTimeout(timeout);
217
270
  try {
218
271
  const response = await fetch(url.toString(), {
219
272
  method,
220
273
  headers,
221
- body: body ? JSON.stringify(body) : void 0,
274
+ body: body !== void 0 ? JSON.stringify(body) : void 0,
222
275
  signal: controller.signal
223
276
  });
224
277
  clearTimeout(timer);
225
- const contentType = response.headers.get("content-type") || "";
226
- let data;
227
- if (contentType.includes("application/json")) {
228
- data = await response.json();
229
- } else {
230
- data = await response.text();
231
- }
278
+ const data = await parseResponseBody(response);
232
279
  if (!response.ok) {
233
- const errorBody = data;
234
- const message = errorBody?.error || errorBody?.message || `HTTP ${response.status}`;
235
- const code = errorBody?.code;
236
- if (response.status === 401 || response.status === 403) {
237
- throw new AuthError(message);
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
- }
246
- throw new ApiError(message, response.status, code);
280
+ throwResponseError(response, path2, data);
247
281
  }
248
282
  return data;
249
283
  } catch (err) {
250
284
  clearTimeout(timer);
251
- if (err instanceof AuthError || err instanceof ApiError) {
252
- throw err;
253
- }
254
- if (err instanceof DOMException && err.name === "AbortError") {
255
- throw new NetworkError(`Request timed out after ${timeout / 1e3}s`);
256
- }
257
- if (err instanceof TypeError) {
258
- throw new NetworkError();
285
+ normalizeRequestError(err, timeout);
286
+ }
287
+ }
288
+ async function fetchApiBinary(method, path2, options = {}) {
289
+ const { body, params, timeout = DEFAULT_TIMEOUT, noAuth = false, adminAuth = false } = options;
290
+ const apiKey = resolveApiKey(noAuth, adminAuth);
291
+ const url = buildUrl(path2, params);
292
+ const headers = buildHeaders(apiKey, body !== void 0);
293
+ const { controller, timer } = createTimeout(timeout);
294
+ try {
295
+ const response = await fetch(url.toString(), {
296
+ method,
297
+ headers,
298
+ body: body !== void 0 ? JSON.stringify(body) : void 0,
299
+ signal: controller.signal
300
+ });
301
+ clearTimeout(timer);
302
+ if (!response.ok) {
303
+ const data = await parseResponseBody(response);
304
+ throwResponseError(response, path2, data);
259
305
  }
260
- throw err;
306
+ return {
307
+ bytes: new Uint8Array(await response.arrayBuffer()),
308
+ headers: response.headers
309
+ };
310
+ } catch (err) {
311
+ clearTimeout(timer);
312
+ normalizeRequestError(err, timeout);
261
313
  }
262
314
  }
263
315
 
@@ -1189,6 +1241,10 @@ function registerWaitlistCommand(program2) {
1189
1241
  });
1190
1242
  }
1191
1243
 
1244
+ // src/commands/admin/admin-orders.ts
1245
+ import { dirname, resolve } from "path";
1246
+ import { statSync, writeFileSync } from "fs";
1247
+
1192
1248
  // src/commands/admin/admin-utils.ts
1193
1249
  import * as readline6 from "readline/promises";
1194
1250
  async function confirm(message) {
@@ -1212,6 +1268,35 @@ var ORDER_COLUMNS2 = [
1212
1268
  { key: "paymentProvider", header: "Provider", width: 12 },
1213
1269
  { key: "createdAt", header: "Created", width: 22 }
1214
1270
  ];
1271
+ function assertInvoiceOutputPath(output) {
1272
+ const resolved = resolve(output);
1273
+ const parent = dirname(resolved);
1274
+ try {
1275
+ if (!statSync(parent).isDirectory()) {
1276
+ throw new CliError(`Output parent is not a directory: ${parent}`);
1277
+ }
1278
+ } catch (err) {
1279
+ const code = err && typeof err === "object" && "code" in err ? err.code : void 0;
1280
+ if (code === "ENOENT") {
1281
+ throw new CliError(`Output directory does not exist: ${parent}`);
1282
+ }
1283
+ if (err instanceof CliError) throw err;
1284
+ throw err;
1285
+ }
1286
+ try {
1287
+ const existing = statSync(resolved);
1288
+ if (existing.isDirectory()) {
1289
+ throw new CliError(`Output path is a directory: ${resolved}`);
1290
+ }
1291
+ throw new CliError(`Output file already exists: ${resolved}`);
1292
+ } catch (err) {
1293
+ const code = err && typeof err === "object" && "code" in err ? err.code : void 0;
1294
+ if (code === "ENOENT") {
1295
+ return resolved;
1296
+ }
1297
+ throw err;
1298
+ }
1299
+ }
1215
1300
  function registerAdminOrdersCommand(admin) {
1216
1301
  const orders = admin.command("orders").description("Admin order management");
1217
1302
  orders.command("list").description("List all orders").option("--provider <name>", "Filter by payment provider").action(async function() {
@@ -1252,6 +1337,18 @@ function registerAdminOrdersCommand(admin) {
1252
1337
  handleError(err);
1253
1338
  }
1254
1339
  });
1340
+ orders.command("invoice <id>").description("Download order invoice PDF").requiredOption("--output <file>", "Output PDF path").action(async function(id) {
1341
+ try {
1342
+ ensureAdminAuth();
1343
+ validateId(id, "order ID");
1344
+ const outputPath = assertInvoiceOutputPath(this.opts().output);
1345
+ const result = await fetchApiBinary("GET", `/admin/orders/${id}/pdf`, { adminAuth: true });
1346
+ writeFileSync(outputPath, Buffer.from(result.bytes), { flag: "wx" });
1347
+ printSuccess(`Invoice saved to ${outputPath}`);
1348
+ } catch (err) {
1349
+ handleError(err);
1350
+ }
1351
+ });
1255
1352
  orders.command("complete <id>").description("Mark an order as complete").action(async function(id) {
1256
1353
  try {
1257
1354
  ensureAdminAuth();
@@ -1483,6 +1580,33 @@ function registerAdminUsersCommand(admin) {
1483
1580
  handleError(err);
1484
1581
  }
1485
1582
  });
1583
+ users.command("delete-orphan <id>").description("Hard-delete a safe orphan user").requiredOption("--reason <text>", "Audit reason for deleting the orphan user").action(async function(id) {
1584
+ try {
1585
+ ensureAdminAuth();
1586
+ validateId(id, "user ID");
1587
+ const { reason } = this.opts();
1588
+ if (!reason || String(reason).trim().length === 0) {
1589
+ throw new CliError("--reason is required.");
1590
+ }
1591
+ const ok = await confirm(`Hard-delete orphan user ${id}? This cannot be undone.`);
1592
+ if (!ok) {
1593
+ console.log("Aborted.");
1594
+ return;
1595
+ }
1596
+ const data = await fetchApi("DELETE", `/admin/users/${id}/orphan`, {
1597
+ adminAuth: true,
1598
+ body: { reason }
1599
+ });
1600
+ const outputOpts = getOutputOpts(this);
1601
+ if (outputOpts.json) {
1602
+ formatOutput(data, outputOpts);
1603
+ return;
1604
+ }
1605
+ printSuccess(`Orphan user ${id} deleted.`);
1606
+ } catch (err) {
1607
+ handleError(err);
1608
+ }
1609
+ });
1486
1610
  users.command("export").description("Export users as CSV").option("--start <date>", "Start date (ISO 8601)").option("--end <date>", "End date (ISO 8601)").option("--search <text>", "Search text").action(async function() {
1487
1611
  try {
1488
1612
  ensureAdminAuth();
@@ -3003,7 +3127,7 @@ function registerAdminCommand(program2) {
3003
3127
  // src/index.ts
3004
3128
  loadDotenvFiles();
3005
3129
  var program = new Command();
3006
- program.name("ckweb").description("CLI for interacting with ClaudeKit.cc API").version("0.5.0");
3130
+ program.name("ckweb").description("CLI for interacting with ClaudeKit.cc API").version("0.6.0-beta.0");
3007
3131
  program.option("--json", "Output as JSON").option("--table", "Output as table").option("--quiet", "Minimal output");
3008
3132
  registerAuthCommand(program);
3009
3133
  registerHealthCommand(program);