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 +23 -3
- package/dist/index.js +160 -36
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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,
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
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.
|
|
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);
|