@supercheck/cli 0.1.1-beta.2 → 0.1.1-rc.1
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 +39 -19
- package/dist/bin/supercheck.js +293 -74
- package/dist/bin/supercheck.js.map +1 -1
- package/package.json +5 -2
package/dist/bin/supercheck.js
CHANGED
|
@@ -44,6 +44,12 @@ var TimeoutError = class extends CLIError {
|
|
|
44
44
|
this.name = "TimeoutError";
|
|
45
45
|
}
|
|
46
46
|
};
|
|
47
|
+
var ConfigNotFoundError = class extends CLIError {
|
|
48
|
+
constructor(message = "No supercheck.config.ts found. Run `supercheck init` to create one.") {
|
|
49
|
+
super(message, 3 /* ConfigError */);
|
|
50
|
+
this.name = "ConfigNotFoundError";
|
|
51
|
+
}
|
|
52
|
+
};
|
|
47
53
|
|
|
48
54
|
// src/utils/logger.ts
|
|
49
55
|
import pc from "picocolors";
|
|
@@ -216,7 +222,7 @@ function requireTriggerKey() {
|
|
|
216
222
|
}
|
|
217
223
|
|
|
218
224
|
// src/version.ts
|
|
219
|
-
var CLI_VERSION = true ? "0.1.1-
|
|
225
|
+
var CLI_VERSION = true ? "0.1.1-rc.1" : "0.0.0-dev";
|
|
220
226
|
|
|
221
227
|
// src/utils/proxy.ts
|
|
222
228
|
import { ProxyAgent } from "undici";
|
|
@@ -314,12 +320,20 @@ var ApiClient = class {
|
|
|
314
320
|
}
|
|
315
321
|
/**
|
|
316
322
|
* Execute an HTTP request with retries, timeout, and rate limit handling.
|
|
323
|
+
*
|
|
324
|
+
* Retry policy:
|
|
325
|
+
* - 429 (rate limit): retried for all methods (with Retry-After header)
|
|
326
|
+
* - 5xx (server error): retried only for idempotent methods (GET, PUT, DELETE, HEAD, OPTIONS)
|
|
327
|
+
* - Network errors: retried only for idempotent methods
|
|
328
|
+
* - POST and PATCH are NOT retried on 5xx/network errors to prevent duplicate mutations
|
|
317
329
|
*/
|
|
318
330
|
async request(method, path, options) {
|
|
319
331
|
const url = this.buildUrl(path, options?.params);
|
|
320
332
|
const parsedUrl = new URL(url);
|
|
321
333
|
const headers = this.buildHeaders(options?.headers);
|
|
322
334
|
const maxRetries = options?.retries ?? MAX_RETRIES;
|
|
335
|
+
const upperMethod = method.toUpperCase();
|
|
336
|
+
const isIdempotent = ["GET", "PUT", "DELETE", "HEAD", "OPTIONS"].includes(upperMethod);
|
|
323
337
|
let lastError = null;
|
|
324
338
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
325
339
|
let timeoutId = null;
|
|
@@ -353,7 +367,7 @@ var ApiClient = class {
|
|
|
353
367
|
await this.sleep(waitMs);
|
|
354
368
|
continue;
|
|
355
369
|
}
|
|
356
|
-
if (response.status >= 500 && attempt < maxRetries) {
|
|
370
|
+
if (response.status >= 500 && attempt < maxRetries && isIdempotent) {
|
|
357
371
|
const waitMs = RETRY_BACKOFF_MS * Math.pow(2, attempt);
|
|
358
372
|
logger.debug(`Server error ${response.status}. Retrying in ${waitMs}ms...`);
|
|
359
373
|
await this.sleep(waitMs);
|
|
@@ -385,7 +399,7 @@ var ApiClient = class {
|
|
|
385
399
|
throw new TimeoutError(`Request timed out after ${this.timeout}ms: ${method} ${path}`);
|
|
386
400
|
}
|
|
387
401
|
lastError = err;
|
|
388
|
-
if (attempt < maxRetries) {
|
|
402
|
+
if (attempt < maxRetries && isIdempotent) {
|
|
389
403
|
const waitMs = RETRY_BACKOFF_MS * Math.pow(2, attempt);
|
|
390
404
|
logger.debug(`Network error: ${err.message}. Retrying in ${waitMs}ms...`);
|
|
391
405
|
await this.sleep(waitMs);
|
|
@@ -691,6 +705,7 @@ function normalizeAlertConfig(alert) {
|
|
|
691
705
|
function normalizeMonitorConfig(config) {
|
|
692
706
|
const normalized = { ...config };
|
|
693
707
|
delete normalized.sslLastCheckedAt;
|
|
708
|
+
delete normalized.aggregatedAlertState;
|
|
694
709
|
if (normalized.playwrightOptions && typeof normalized.playwrightOptions === "object") {
|
|
695
710
|
const opts = { ...normalized.playwrightOptions };
|
|
696
711
|
if (opts.retries === 0) delete opts.retries;
|
|
@@ -732,8 +747,8 @@ function normalizeRemoteRaw(type, raw) {
|
|
|
732
747
|
}
|
|
733
748
|
}
|
|
734
749
|
if (type === "test") {
|
|
735
|
-
if (normalized.type === "
|
|
736
|
-
else
|
|
750
|
+
if (normalized.type === "performance") normalized.testType = "k6";
|
|
751
|
+
else normalized.testType = "playwright";
|
|
737
752
|
delete normalized.type;
|
|
738
753
|
if (typeof normalized.script === "string") {
|
|
739
754
|
normalized.script = stripTitleMetadata(normalized.script);
|
|
@@ -1013,7 +1028,12 @@ function formatValue(value, column, row) {
|
|
|
1013
1028
|
return value ? pc2.green("\u2713") : pc2.red("\u2717");
|
|
1014
1029
|
}
|
|
1015
1030
|
if (value instanceof Date) return formatDate(value);
|
|
1016
|
-
if (Array.isArray(value))
|
|
1031
|
+
if (Array.isArray(value)) {
|
|
1032
|
+
if (value.length > 0 && typeof value[0] === "object" && value[0] !== null) {
|
|
1033
|
+
return JSON.stringify(value);
|
|
1034
|
+
}
|
|
1035
|
+
return value.join(", ");
|
|
1036
|
+
}
|
|
1017
1037
|
if (typeof value === "number") {
|
|
1018
1038
|
const dateForNumber = maybeFormatDate(value, column?.key);
|
|
1019
1039
|
if (dateForNumber) return dateForNumber;
|
|
@@ -1044,6 +1064,22 @@ function maybeFormatDate(value, key) {
|
|
|
1044
1064
|
function formatDate(date) {
|
|
1045
1065
|
return date.toISOString().replace("T", " ").replace("Z", "");
|
|
1046
1066
|
}
|
|
1067
|
+
function summarizeArray(_key, items) {
|
|
1068
|
+
if (items.length === 0) return pc2.dim("(none)");
|
|
1069
|
+
if (typeof items[0] !== "object" || items[0] === null) {
|
|
1070
|
+
return items.join(", ");
|
|
1071
|
+
}
|
|
1072
|
+
const previews = [];
|
|
1073
|
+
for (const item of items.slice(0, 3)) {
|
|
1074
|
+
const obj = item;
|
|
1075
|
+
const name = obj.name ?? obj.title ?? obj.key ?? obj.id;
|
|
1076
|
+
if (name) previews.push(String(name));
|
|
1077
|
+
}
|
|
1078
|
+
const countStr = `${items.length} ${items.length === 1 ? "item" : "items"}`;
|
|
1079
|
+
if (previews.length === 0) return countStr;
|
|
1080
|
+
const previewStr = previews.join(", ");
|
|
1081
|
+
return items.length > 3 ? `${countStr} (${previewStr}, ...)` : `${countStr} (${previewStr})`;
|
|
1082
|
+
}
|
|
1047
1083
|
function outputDetail(data) {
|
|
1048
1084
|
if (currentFormat === "json") {
|
|
1049
1085
|
logger.output(JSON.stringify(data, null, 2));
|
|
@@ -1058,10 +1094,47 @@ function outputDetail(data) {
|
|
|
1058
1094
|
logger.info("No details available.");
|
|
1059
1095
|
return;
|
|
1060
1096
|
}
|
|
1061
|
-
const
|
|
1097
|
+
const scalarEntries = [];
|
|
1098
|
+
const nestedEntries = [];
|
|
1062
1099
|
for (const [key, value] of Object.entries(data)) {
|
|
1063
|
-
|
|
1064
|
-
|
|
1100
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date)) {
|
|
1101
|
+
nestedEntries.push([key, value]);
|
|
1102
|
+
} else {
|
|
1103
|
+
scalarEntries.push([key, value]);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
if (scalarEntries.length > 0) {
|
|
1107
|
+
const maxKeyLen = Math.max(...scalarEntries.map(([k]) => k.length));
|
|
1108
|
+
for (const [key, value] of scalarEntries) {
|
|
1109
|
+
const label = key.padEnd(maxKeyLen + 2);
|
|
1110
|
+
let formatted;
|
|
1111
|
+
if (Array.isArray(value)) {
|
|
1112
|
+
formatted = summarizeArray(key, value);
|
|
1113
|
+
} else {
|
|
1114
|
+
formatted = formatValue(value, { key, header: key }, data);
|
|
1115
|
+
}
|
|
1116
|
+
logger.output(` ${label}${formatted}`);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
for (const [key, obj] of nestedEntries) {
|
|
1120
|
+
logger.output("");
|
|
1121
|
+
logger.output(` ${pc2.bold(key)}:`);
|
|
1122
|
+
const subKeys = Object.keys(obj);
|
|
1123
|
+
if (subKeys.length === 0) {
|
|
1124
|
+
logger.output(` ${pc2.dim("(empty)")}`);
|
|
1125
|
+
continue;
|
|
1126
|
+
}
|
|
1127
|
+
const maxSubKeyLen = Math.max(...subKeys.map((k) => k.length));
|
|
1128
|
+
for (const [subKey, subVal] of Object.entries(obj)) {
|
|
1129
|
+
const subLabel = subKey.padEnd(maxSubKeyLen + 2);
|
|
1130
|
+
let formatted;
|
|
1131
|
+
if (Array.isArray(subVal)) {
|
|
1132
|
+
formatted = summarizeArray(subKey, subVal);
|
|
1133
|
+
} else {
|
|
1134
|
+
formatted = formatValue(subVal, { key: subKey, header: subKey }, obj);
|
|
1135
|
+
}
|
|
1136
|
+
logger.output(` ${subLabel}${formatted}`);
|
|
1137
|
+
}
|
|
1065
1138
|
}
|
|
1066
1139
|
}
|
|
1067
1140
|
|
|
@@ -1168,6 +1241,18 @@ var whoamiCommand = new Command("whoami").description("Show current authenticati
|
|
|
1168
1241
|
);
|
|
1169
1242
|
const tokenPreview = safeTokenPreview(token);
|
|
1170
1243
|
const activeToken = data.tokens?.find((t) => t.start && token.startsWith(t.start.replace(/\.+$/, "")));
|
|
1244
|
+
if (getOutputFormat() === "json") {
|
|
1245
|
+
const jsonData = {
|
|
1246
|
+
user: activeToken?.createdByName ?? null,
|
|
1247
|
+
tokenName: activeToken?.name ?? null,
|
|
1248
|
+
tokenPreview,
|
|
1249
|
+
apiUrl: baseUrl ?? "https://app.supercheck.io",
|
|
1250
|
+
expiresAt: activeToken?.expiresAt ?? null,
|
|
1251
|
+
lastUsed: activeToken?.lastRequest ?? null
|
|
1252
|
+
};
|
|
1253
|
+
logger.output(JSON.stringify(jsonData, null, 2));
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1171
1256
|
logger.newline();
|
|
1172
1257
|
logger.header("Current Context");
|
|
1173
1258
|
logger.newline();
|
|
@@ -1405,7 +1490,9 @@ function validateNoSecrets(config) {
|
|
|
1405
1490
|
const secretPatterns = [
|
|
1406
1491
|
/sck_live_[a-zA-Z0-9]+/,
|
|
1407
1492
|
/sck_trigger_[a-zA-Z0-9]+/,
|
|
1408
|
-
/sck_test_[a-zA-Z0-9]
|
|
1493
|
+
/sck_test_[a-zA-Z0-9]+/,
|
|
1494
|
+
// Legacy trigger key format: job_ followed by 32+ hex chars
|
|
1495
|
+
/job_[a-fA-F0-9]{32,}/
|
|
1409
1496
|
];
|
|
1410
1497
|
for (const pattern of secretPatterns) {
|
|
1411
1498
|
if (pattern.test(configStr)) {
|
|
@@ -1419,13 +1506,10 @@ function validateNoSecrets(config) {
|
|
|
1419
1506
|
async function loadConfig(options = {}) {
|
|
1420
1507
|
const cwd = options.cwd ?? process.cwd();
|
|
1421
1508
|
const { config: loadEnv } = await import("dotenv");
|
|
1422
|
-
loadEnv({ path: resolve2(cwd, ".env") });
|
|
1509
|
+
loadEnv({ path: resolve2(cwd, ".env"), quiet: true });
|
|
1423
1510
|
const configPath = resolveConfigPath(cwd, options.configPath);
|
|
1424
1511
|
if (!configPath) {
|
|
1425
|
-
throw new
|
|
1426
|
-
"No supercheck.config.ts found. Run `supercheck init` to create one.",
|
|
1427
|
-
3 /* ConfigError */
|
|
1428
|
-
);
|
|
1512
|
+
throw new ConfigNotFoundError();
|
|
1429
1513
|
}
|
|
1430
1514
|
let config = await loadConfigFile(configPath);
|
|
1431
1515
|
const localConfigPath = resolveLocalConfigPath(cwd);
|
|
@@ -1453,7 +1537,7 @@ async function tryLoadConfig(options = {}) {
|
|
|
1453
1537
|
try {
|
|
1454
1538
|
return await loadConfig(options);
|
|
1455
1539
|
} catch (err) {
|
|
1456
|
-
if (err instanceof
|
|
1540
|
+
if (err instanceof ConfigNotFoundError) {
|
|
1457
1541
|
return null;
|
|
1458
1542
|
}
|
|
1459
1543
|
throw err;
|
|
@@ -2005,9 +2089,10 @@ import { readFileSync as readFileSync3 } from "fs";
|
|
|
2005
2089
|
import { resolve as resolve6 } from "path";
|
|
2006
2090
|
|
|
2007
2091
|
// src/api/authenticated-client.ts
|
|
2008
|
-
function createAuthenticatedClient() {
|
|
2092
|
+
function createAuthenticatedClient(configBaseUrl) {
|
|
2009
2093
|
const token = requireAuth();
|
|
2010
|
-
const
|
|
2094
|
+
const storedBaseUrl = getStoredBaseUrl();
|
|
2095
|
+
const baseUrl = storedBaseUrl ?? configBaseUrl;
|
|
2011
2096
|
return getApiClient({ token, baseUrl: baseUrl ?? void 0 });
|
|
2012
2097
|
}
|
|
2013
2098
|
|
|
@@ -2036,6 +2121,21 @@ function parseIntStrict(value, name, opts) {
|
|
|
2036
2121
|
}
|
|
2037
2122
|
|
|
2038
2123
|
// src/utils/validation.ts
|
|
2124
|
+
var K6_IMPORT_PATTERN = /import\s+.*\s+from\s+['"]k6(?:\/[^'"]*)?['"]/;
|
|
2125
|
+
function isK6Script(script) {
|
|
2126
|
+
return K6_IMPORT_PATTERN.test(script);
|
|
2127
|
+
}
|
|
2128
|
+
function validateScriptTypeMatch(script, declaredType) {
|
|
2129
|
+
if (!script || !declaredType) return void 0;
|
|
2130
|
+
const scriptIsK6 = isK6Script(script);
|
|
2131
|
+
if (scriptIsK6 && declaredType !== "performance") {
|
|
2132
|
+
return `Script contains k6 imports but test type is "${declaredType}". k6 scripts must use type "performance".`;
|
|
2133
|
+
}
|
|
2134
|
+
if (!scriptIsK6 && declaredType === "performance") {
|
|
2135
|
+
return 'Test type is "performance" but script does not contain k6 imports. Performance tests require k6 scripts.';
|
|
2136
|
+
}
|
|
2137
|
+
return void 0;
|
|
2138
|
+
}
|
|
2039
2139
|
function normalizeTestTypeForApi(localType) {
|
|
2040
2140
|
if (typeof localType !== "string") return void 0;
|
|
2041
2141
|
const normalized = localType.trim().toLowerCase();
|
|
@@ -2170,6 +2270,13 @@ function resolveJobLocalTests(cwd, tests, patterns) {
|
|
|
2170
2270
|
});
|
|
2171
2271
|
}
|
|
2172
2272
|
async function validateJobTests(client, tests) {
|
|
2273
|
+
for (const test of tests) {
|
|
2274
|
+
const declaredType = normalizeTestTypeForApi(test.type);
|
|
2275
|
+
const mismatch = validateScriptTypeMatch(test.script, declaredType);
|
|
2276
|
+
if (mismatch) {
|
|
2277
|
+
throw new CLIError(`${test.name}: ${mismatch}`, 3 /* ConfigError */);
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2173
2280
|
const inputs = tests.map((test) => ({
|
|
2174
2281
|
name: test.name,
|
|
2175
2282
|
script: test.script,
|
|
@@ -2297,7 +2404,7 @@ jobCommand.command("get <id>").description("Get job details").action(async (id)
|
|
|
2297
2404
|
);
|
|
2298
2405
|
outputDetail(data);
|
|
2299
2406
|
});
|
|
2300
|
-
jobCommand.command("create").description("Create a new job").requiredOption("--name <name>", "Job name").requiredOption("--tests <tests...>", "Test IDs (space or comma separated)").option("--description <description>", "Job description", "").option("--type <type>", "Job runner type (playwright, k6)").option("--schedule <cron>", "Cron schedule expression").option("--
|
|
2407
|
+
jobCommand.command("create").description("Create a new job").requiredOption("--name <name>", "Job name").requiredOption("--tests <tests...>", "Test IDs (space or comma separated)").option("--description <description>", "Job description", "").option("--type <type>", "Job runner type (playwright, k6)").option("--schedule <cron>", "Cron schedule expression").option("--dry-run", "Show what would be sent without creating").action(async (options) => {
|
|
2301
2408
|
const client = createAuthenticatedClient();
|
|
2302
2409
|
const tests = (options.tests ?? []).flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean);
|
|
2303
2410
|
if (tests.length === 0) {
|
|
@@ -2306,13 +2413,16 @@ jobCommand.command("create").description("Create a new job").requiredOption("--n
|
|
|
2306
2413
|
const body = {
|
|
2307
2414
|
name: options.name,
|
|
2308
2415
|
description: options.description,
|
|
2309
|
-
timeoutSeconds: parseIntStrict(options.timeout, "--timeout", { min: 1 }),
|
|
2310
|
-
retryCount: parseIntStrict(options.retries, "--retries", { min: 0 }),
|
|
2311
2416
|
config: {},
|
|
2312
2417
|
tests: tests.map((id) => ({ id }))
|
|
2313
2418
|
};
|
|
2314
2419
|
if (options.type) body.jobType = options.type;
|
|
2315
2420
|
if (options.schedule) body.cronSchedule = options.schedule;
|
|
2421
|
+
if (options.dryRun) {
|
|
2422
|
+
logger.header("Dry run \u2014 job create payload:");
|
|
2423
|
+
logger.info(JSON.stringify(body, null, 2));
|
|
2424
|
+
return;
|
|
2425
|
+
}
|
|
2316
2426
|
const { data } = await withSpinner(
|
|
2317
2427
|
"Creating job",
|
|
2318
2428
|
() => client.post("/api/jobs", body)
|
|
@@ -2322,17 +2432,20 @@ jobCommand.command("create").description("Create a new job").requiredOption("--n
|
|
|
2322
2432
|
logger.success(`Job "${options.name}" created (${jobId ?? "unknown"})`);
|
|
2323
2433
|
outputDetail(job);
|
|
2324
2434
|
});
|
|
2325
|
-
jobCommand.command("update <id>").description("Update job configuration").option("--name <name>", "Job name").option("--description <description>", "Job description").option("--schedule <cron>", "Cron schedule expression").option("--
|
|
2435
|
+
jobCommand.command("update <id>").description("Update job configuration").option("--name <name>", "Job name").option("--description <description>", "Job description").option("--schedule <cron>", "Cron schedule expression").option("--status <status>", "Job status (active, paused)").option("--dry-run", "Show what would be sent without updating").action(async (id, options) => {
|
|
2326
2436
|
const client = createAuthenticatedClient();
|
|
2327
2437
|
const body = {};
|
|
2328
2438
|
if (options.name !== void 0) body.name = options.name;
|
|
2329
2439
|
if (options.description !== void 0) body.description = options.description;
|
|
2330
2440
|
if (options.schedule !== void 0) body.cronSchedule = options.schedule;
|
|
2331
|
-
if (options.timeout !== void 0) body.timeoutSeconds = parseIntStrict(options.timeout, "--timeout", { min: 1 });
|
|
2332
|
-
if (options.retries !== void 0) body.retryCount = parseIntStrict(options.retries, "--retries", { min: 0 });
|
|
2333
2441
|
if (options.status !== void 0) body.status = options.status;
|
|
2334
2442
|
if (Object.keys(body).length === 0) {
|
|
2335
|
-
logger.warn("No fields to update. Use --name, --description, --schedule,
|
|
2443
|
+
logger.warn("No fields to update. Use --name, --description, --schedule, or --status.");
|
|
2444
|
+
return;
|
|
2445
|
+
}
|
|
2446
|
+
if (options.dryRun) {
|
|
2447
|
+
logger.header("Dry run \u2014 job update payload:");
|
|
2448
|
+
logger.info(JSON.stringify(body, null, 2));
|
|
2336
2449
|
return;
|
|
2337
2450
|
}
|
|
2338
2451
|
const { data } = await withSpinner(
|
|
@@ -2694,6 +2807,13 @@ function collectLocalTests(cwd, patterns, options) {
|
|
|
2694
2807
|
});
|
|
2695
2808
|
}
|
|
2696
2809
|
async function validateLocalTests(client, tests) {
|
|
2810
|
+
for (const test of tests) {
|
|
2811
|
+
const declaredType = test.validationType ?? normalizeTestTypeForApi(test.type);
|
|
2812
|
+
const mismatch = validateScriptTypeMatch(test.script, declaredType);
|
|
2813
|
+
if (mismatch) {
|
|
2814
|
+
throw new CLIError(`${test.name}: ${mismatch}`, 3 /* ConfigError */);
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2697
2817
|
const inputs = tests.map((test) => ({
|
|
2698
2818
|
name: test.name,
|
|
2699
2819
|
script: test.script,
|
|
@@ -2773,7 +2893,7 @@ testCommand.command("get <id>").description("Get test details").option("--includ
|
|
|
2773
2893
|
);
|
|
2774
2894
|
outputDetail(data);
|
|
2775
2895
|
});
|
|
2776
|
-
testCommand.command("create").description("Create a new test").requiredOption("--title <title>", "Test title").requiredOption("--file <path>", "Path to the test script file").option("--type <type>", "Test type (browser, performance, api, database, custom)").option("--description <description>", "Test description").action(async (options) => {
|
|
2896
|
+
testCommand.command("create").description("Create a new test").requiredOption("--title <title>", "Test title").requiredOption("--file <path>", "Path to the test script file").option("--type <type>", "Test type (browser, performance, api, database, custom)").option("--description <description>", "Test description").option("--dry-run", "Show what would be sent without creating").action(async (options) => {
|
|
2777
2897
|
const { readFileSync: readFileSync6 } = await import("fs");
|
|
2778
2898
|
const { resolve: resolve10 } = await import("path");
|
|
2779
2899
|
const filePath = resolve10(process.cwd(), options.file);
|
|
@@ -2784,7 +2904,10 @@ testCommand.command("create").description("Create a new test").requiredOption("-
|
|
|
2784
2904
|
throw new CLIError(`Cannot read file: ${filePath}`, 1 /* GeneralError */);
|
|
2785
2905
|
}
|
|
2786
2906
|
const typeArg = options.type || inferTestType(options.file) || "playwright";
|
|
2787
|
-
const
|
|
2907
|
+
const typeMismatchError = validateScriptTypeMatch(script, normalizeTestTypeForApi(typeArg));
|
|
2908
|
+
if (typeMismatchError) {
|
|
2909
|
+
throw new CLIError(typeMismatchError, 3 /* ConfigError */);
|
|
2910
|
+
}
|
|
2788
2911
|
const encodedScript = Buffer2.from(script, "utf-8").toString("base64");
|
|
2789
2912
|
const body = {
|
|
2790
2913
|
title: options.title,
|
|
@@ -2792,6 +2915,13 @@ testCommand.command("create").description("Create a new test").requiredOption("-
|
|
|
2792
2915
|
type: normalizeTestType(typeArg)
|
|
2793
2916
|
};
|
|
2794
2917
|
if (options.description) body.description = options.description;
|
|
2918
|
+
if (options.dryRun) {
|
|
2919
|
+
const preview = { ...body, script: `<base64 ${encodedScript.length} chars>` };
|
|
2920
|
+
logger.header("Dry run \u2014 test create payload:");
|
|
2921
|
+
logger.info(JSON.stringify(preview, null, 2));
|
|
2922
|
+
return;
|
|
2923
|
+
}
|
|
2924
|
+
const client = createAuthenticatedClient();
|
|
2795
2925
|
const { data } = await withSpinner(
|
|
2796
2926
|
"Creating test",
|
|
2797
2927
|
() => client.post("/api/tests", body)
|
|
@@ -2800,7 +2930,7 @@ testCommand.command("create").description("Create a new test").requiredOption("-
|
|
|
2800
2930
|
logger.success(`Test "${options.title}" created${createdId ? ` (${createdId})` : ""}`);
|
|
2801
2931
|
outputDetail(data);
|
|
2802
2932
|
});
|
|
2803
|
-
testCommand.command("update <id>").description("Update a test").option("--title <title>", "Test title").option("--file <path>", "Path to updated test script").option("--description <description>", "Test description").action(async (id, options) => {
|
|
2933
|
+
testCommand.command("update <id>").description("Update a test").option("--title <title>", "Test title").option("--file <path>", "Path to updated test script").option("--description <description>", "Test description").option("--dry-run", "Show what would be sent without updating").action(async (id, options) => {
|
|
2804
2934
|
const client = createAuthenticatedClient();
|
|
2805
2935
|
const body = {};
|
|
2806
2936
|
if (options.title !== void 0) body.title = options.title;
|
|
@@ -2820,6 +2950,15 @@ testCommand.command("update <id>").description("Update a test").option("--title
|
|
|
2820
2950
|
logger.warn("No fields to update. Use --title, --file, or --description.");
|
|
2821
2951
|
return;
|
|
2822
2952
|
}
|
|
2953
|
+
if (options.dryRun) {
|
|
2954
|
+
const preview = { ...body };
|
|
2955
|
+
if (typeof preview.script === "string") {
|
|
2956
|
+
preview.script = `<base64 ${preview.script.length} chars>`;
|
|
2957
|
+
}
|
|
2958
|
+
logger.header("Dry run \u2014 test update payload:");
|
|
2959
|
+
logger.info(JSON.stringify(preview, null, 2));
|
|
2960
|
+
return;
|
|
2961
|
+
}
|
|
2823
2962
|
const { data } = await withSpinner(
|
|
2824
2963
|
"Updating test",
|
|
2825
2964
|
() => client.patch(`/api/tests/${id}`, body)
|
|
@@ -2843,16 +2982,7 @@ testCommand.command("delete <id>").description("Delete a test").option("--force"
|
|
|
2843
2982
|
);
|
|
2844
2983
|
logger.success(`Test ${id} deleted`);
|
|
2845
2984
|
});
|
|
2846
|
-
testCommand.command("run").description("Run tests locally").option("--
|
|
2847
|
-
if (options.local && options.cloud) {
|
|
2848
|
-
throw new CLIError("--local and --cloud are mutually exclusive.", 3 /* ConfigError */);
|
|
2849
|
-
}
|
|
2850
|
-
if (options.cloud || options.id || options.location) {
|
|
2851
|
-
throw new CLIError(
|
|
2852
|
-
"Cloud execution for single tests has been removed from CLI because results are not persisted as job runs. Use `supercheck test run --local ...` for local validation or create/trigger a job (`supercheck job run` / `supercheck job trigger`) for remote executions with run history.",
|
|
2853
|
-
3 /* ConfigError */
|
|
2854
|
-
);
|
|
2855
|
-
}
|
|
2985
|
+
testCommand.command("run").description("Run tests locally").option("--file <path>", "Local test file path").option("--all", "Run all local tests").option("--type <type>", "Test type filter (browser, performance, api, database, custom)").action(async (options) => {
|
|
2856
2986
|
if (!options.file && !options.all) {
|
|
2857
2987
|
throw new CLIError("Local runs require --file <path> or --all.", 3 /* ConfigError */);
|
|
2858
2988
|
}
|
|
@@ -2899,12 +3029,6 @@ testCommand.command("run").description("Run tests locally").option("--local", "R
|
|
|
2899
3029
|
await runK6Tests(k6Tests, cwd);
|
|
2900
3030
|
}
|
|
2901
3031
|
});
|
|
2902
|
-
testCommand.command("execute <id>").description("DEPRECATED: test execute has been removed").action(async () => {
|
|
2903
|
-
throw new CLIError(
|
|
2904
|
-
"The `test execute` command has been removed. Use `supercheck test run --local ...` for local validation or create/trigger a job (`supercheck job run` / `supercheck job trigger`) for remote executions with run history.",
|
|
2905
|
-
3 /* ConfigError */
|
|
2906
|
-
);
|
|
2907
|
-
});
|
|
2908
3032
|
testCommand.command("tags <id>").description("Get test tags").action(async (id) => {
|
|
2909
3033
|
const client = createAuthenticatedClient();
|
|
2910
3034
|
const { data } = await withSpinner(
|
|
@@ -2929,6 +3053,11 @@ testCommand.command("validate").description("Validate a test script").requiredOp
|
|
|
2929
3053
|
throw new CLIError(`Cannot read file: ${filePath}`, 1 /* GeneralError */);
|
|
2930
3054
|
}
|
|
2931
3055
|
const typeArg = options.type || inferTestType(options.file) || "playwright";
|
|
3056
|
+
const resolvedApiType = normalizeTestTypeForApi(typeArg);
|
|
3057
|
+
const typeMismatchError = validateScriptTypeMatch(script, resolvedApiType);
|
|
3058
|
+
if (typeMismatchError) {
|
|
3059
|
+
throw new CLIError(typeMismatchError, 3 /* ConfigError */);
|
|
3060
|
+
}
|
|
2932
3061
|
const client = createAuthenticatedClient();
|
|
2933
3062
|
const results = await withSpinner(
|
|
2934
3063
|
"Validating script",
|
|
@@ -3057,23 +3186,29 @@ monitorCommand.command("get <id>").description("Get monitor details").action(asy
|
|
|
3057
3186
|
);
|
|
3058
3187
|
outputDetail(data);
|
|
3059
3188
|
});
|
|
3060
|
-
monitorCommand.command("results <id>").description("Get monitor check results").option("--limit <limit>", "Number of results", "20").action(async (id, options) => {
|
|
3189
|
+
monitorCommand.command("results <id>").description("Get monitor check results").option("--limit <limit>", "Number of results", "20").option("--page <page>", "Page number", "1").action(async (id, options) => {
|
|
3061
3190
|
const client = createAuthenticatedClient();
|
|
3062
3191
|
const { data } = await withSpinner(
|
|
3063
3192
|
"Fetching monitor results",
|
|
3064
3193
|
() => client.get(
|
|
3065
3194
|
`/api/monitors/${id}/results`,
|
|
3066
|
-
{ limit: options.limit }
|
|
3195
|
+
{ limit: options.limit, page: options.page }
|
|
3067
3196
|
)
|
|
3068
3197
|
);
|
|
3069
|
-
output(data.
|
|
3198
|
+
output(data.data, {
|
|
3070
3199
|
columns: [
|
|
3071
|
-
{ key: "
|
|
3200
|
+
{ key: "checkedAt", header: "Time" },
|
|
3072
3201
|
{ key: "status", header: "Status" },
|
|
3073
3202
|
{ key: "responseTime", header: "Response (ms)" },
|
|
3074
3203
|
{ key: "location", header: "Location" }
|
|
3075
3204
|
]
|
|
3076
3205
|
});
|
|
3206
|
+
if (data.pagination) {
|
|
3207
|
+
logger.info(
|
|
3208
|
+
`
|
|
3209
|
+
Page ${data.pagination.page}/${data.pagination.totalPages} (${data.pagination.total} total)`
|
|
3210
|
+
);
|
|
3211
|
+
}
|
|
3077
3212
|
});
|
|
3078
3213
|
monitorCommand.command("stats <id>").description("Get monitor performance statistics").action(async (id) => {
|
|
3079
3214
|
const client = createAuthenticatedClient();
|
|
@@ -3081,7 +3216,28 @@ monitorCommand.command("stats <id>").description("Get monitor performance statis
|
|
|
3081
3216
|
"Fetching monitor statistics",
|
|
3082
3217
|
() => client.get(`/api/monitors/${id}/stats`)
|
|
3083
3218
|
);
|
|
3084
|
-
|
|
3219
|
+
if (getOutputFormat() === "json") {
|
|
3220
|
+
outputDetail(data);
|
|
3221
|
+
return;
|
|
3222
|
+
}
|
|
3223
|
+
const statsData = data.data ?? data;
|
|
3224
|
+
const period24h = statsData.period24h;
|
|
3225
|
+
const period30d = statsData.period30d;
|
|
3226
|
+
if (period24h) {
|
|
3227
|
+
logger.header("Last 24 Hours");
|
|
3228
|
+
outputDetail(period24h);
|
|
3229
|
+
logger.info("");
|
|
3230
|
+
}
|
|
3231
|
+
if (period30d) {
|
|
3232
|
+
logger.header("Last 30 Days");
|
|
3233
|
+
outputDetail(period30d);
|
|
3234
|
+
logger.info("");
|
|
3235
|
+
}
|
|
3236
|
+
const meta = data.meta;
|
|
3237
|
+
if (meta) {
|
|
3238
|
+
logger.header("Meta");
|
|
3239
|
+
outputDetail(meta);
|
|
3240
|
+
}
|
|
3085
3241
|
});
|
|
3086
3242
|
monitorCommand.command("status <id>").description("Get current monitor status").action(async (id) => {
|
|
3087
3243
|
const client = createAuthenticatedClient();
|
|
@@ -3098,10 +3254,22 @@ monitorCommand.command("status <id>").description("Get current monitor status").
|
|
|
3098
3254
|
};
|
|
3099
3255
|
outputDetail(statusInfo);
|
|
3100
3256
|
});
|
|
3101
|
-
monitorCommand.command("create").description("Create a new monitor").requiredOption("--name <name>", "Monitor name").requiredOption("--url <url>", "URL to monitor").option("--type <type>", "Monitor type (http_request, website, ping_host, port_check, synthetic_test)", "http_request").option("--interval <
|
|
3257
|
+
monitorCommand.command("create").description("Create a new monitor").requiredOption("--name <name>", "Monitor name").requiredOption("--url <url>", "URL to monitor").option("--type <type>", "Monitor type (http_request, website, ping_host, port_check, synthetic_test)", "http_request").option("--interval-minutes <minutes>", "Check interval in minutes (1-1440)", "5").option("--interval <seconds>", "[deprecated: use --interval-minutes] Check interval in seconds").option("--timeout <seconds>", "Request timeout in seconds", "30").option("--method <method>", "HTTP method (GET, POST, HEAD)", "GET").option("--dry-run", "Show what would be sent without creating").action(async (options) => {
|
|
3102
3258
|
const client = createAuthenticatedClient();
|
|
3103
|
-
|
|
3104
|
-
|
|
3259
|
+
let frequencyMinutes;
|
|
3260
|
+
if (options.interval !== void 0) {
|
|
3261
|
+
logger.warn("--interval (seconds) is deprecated. Use --interval-minutes instead.");
|
|
3262
|
+
const intervalSeconds = parseIntStrict(options.interval, "--interval", { min: 60 });
|
|
3263
|
+
if (intervalSeconds % 60 !== 0) {
|
|
3264
|
+
throw new CLIError(
|
|
3265
|
+
`--interval must be a multiple of 60 seconds. Got ${intervalSeconds}s. Use --interval-minutes for direct minute values.`,
|
|
3266
|
+
3 /* ConfigError */
|
|
3267
|
+
);
|
|
3268
|
+
}
|
|
3269
|
+
frequencyMinutes = intervalSeconds / 60;
|
|
3270
|
+
} else {
|
|
3271
|
+
frequencyMinutes = parseIntStrict(options.intervalMinutes, "--interval-minutes", { min: 1, max: 1440 });
|
|
3272
|
+
}
|
|
3105
3273
|
const body = {
|
|
3106
3274
|
name: options.name,
|
|
3107
3275
|
target: options.url,
|
|
@@ -3113,6 +3281,11 @@ monitorCommand.command("create").description("Create a new monitor").requiredOpt
|
|
|
3113
3281
|
method: options.method
|
|
3114
3282
|
}
|
|
3115
3283
|
};
|
|
3284
|
+
if (options.dryRun) {
|
|
3285
|
+
logger.header("Dry run \u2014 monitor create payload:");
|
|
3286
|
+
logger.info(JSON.stringify(body, null, 2));
|
|
3287
|
+
return;
|
|
3288
|
+
}
|
|
3116
3289
|
const { data } = await withSpinner(
|
|
3117
3290
|
"Creating monitor",
|
|
3118
3291
|
() => client.post("/api/monitors", body)
|
|
@@ -3120,14 +3293,23 @@ monitorCommand.command("create").description("Create a new monitor").requiredOpt
|
|
|
3120
3293
|
logger.success(`Monitor "${options.name}" created (${data.id})`);
|
|
3121
3294
|
outputDetail(data);
|
|
3122
3295
|
});
|
|
3123
|
-
monitorCommand.command("update <id>").description("Update a monitor").option("--name <name>", "Monitor name").option("--url <url>", "URL to monitor").option("--interval <seconds>", "Check interval in seconds").option("--timeout <seconds>", "Request timeout in seconds").option("--method <method>", "HTTP method").option("--active <boolean>", "Enable or disable monitor (true/false)").action(async (id, options) => {
|
|
3296
|
+
monitorCommand.command("update <id>").description("Update a monitor").option("--name <name>", "Monitor name").option("--url <url>", "URL to monitor").option("--interval-minutes <minutes>", "Check interval in minutes (1-1440)").option("--interval <seconds>", "[deprecated: use --interval-minutes] Check interval in seconds").option("--timeout <seconds>", "Request timeout in seconds").option("--method <method>", "HTTP method").option("--active <boolean>", "Enable or disable monitor (true/false)").option("--dry-run", "Show what would be sent without updating").action(async (id, options) => {
|
|
3124
3297
|
const client = createAuthenticatedClient();
|
|
3125
3298
|
const body = {};
|
|
3126
3299
|
if (options.name !== void 0) body.name = options.name;
|
|
3127
3300
|
if (options.url !== void 0) body.target = options.url;
|
|
3128
3301
|
if (options.interval !== void 0) {
|
|
3129
|
-
|
|
3130
|
-
|
|
3302
|
+
logger.warn("--interval (seconds) is deprecated. Use --interval-minutes instead.");
|
|
3303
|
+
const intervalSeconds = parseIntStrict(options.interval, "--interval", { min: 60 });
|
|
3304
|
+
if (intervalSeconds % 60 !== 0) {
|
|
3305
|
+
throw new CLIError(
|
|
3306
|
+
`--interval must be a multiple of 60 seconds. Got ${intervalSeconds}s. Use --interval-minutes for direct minute values.`,
|
|
3307
|
+
3 /* ConfigError */
|
|
3308
|
+
);
|
|
3309
|
+
}
|
|
3310
|
+
body.frequencyMinutes = intervalSeconds / 60;
|
|
3311
|
+
} else if (options.intervalMinutes !== void 0) {
|
|
3312
|
+
body.frequencyMinutes = parseIntStrict(options.intervalMinutes, "--interval-minutes", { min: 1, max: 1440 });
|
|
3131
3313
|
}
|
|
3132
3314
|
if (options.active !== void 0) body.enabled = options.active === "true";
|
|
3133
3315
|
const config = {};
|
|
@@ -3135,7 +3317,12 @@ monitorCommand.command("update <id>").description("Update a monitor").option("--
|
|
|
3135
3317
|
if (options.method !== void 0) config.method = options.method;
|
|
3136
3318
|
if (Object.keys(config).length > 0) body.config = config;
|
|
3137
3319
|
if (Object.keys(body).length === 0) {
|
|
3138
|
-
logger.warn("No fields to update. Use --name, --url, --interval, --timeout, --method, or --active.");
|
|
3320
|
+
logger.warn("No fields to update. Use --name, --url, --interval-minutes, --timeout, --method, or --active.");
|
|
3321
|
+
return;
|
|
3322
|
+
}
|
|
3323
|
+
if (options.dryRun) {
|
|
3324
|
+
logger.header("Dry run \u2014 monitor update payload:");
|
|
3325
|
+
logger.info(JSON.stringify(body, null, 2));
|
|
3139
3326
|
return;
|
|
3140
3327
|
}
|
|
3141
3328
|
const { data } = await withSpinner(
|
|
@@ -3438,7 +3625,7 @@ function formatChangePlan(changes) {
|
|
|
3438
3625
|
var diffCommand = new Command11("diff").description("Preview changes between local config and the remote Supercheck project").option("--config <path>", "Path to config file").action(async (options) => {
|
|
3439
3626
|
const cwd = process.cwd();
|
|
3440
3627
|
const { config } = await loadConfig({ cwd, configPath: options.config });
|
|
3441
|
-
const client = createAuthenticatedClient();
|
|
3628
|
+
const client = createAuthenticatedClient(config.api?.baseUrl);
|
|
3442
3629
|
logger.newline();
|
|
3443
3630
|
const { localResources, remoteResources } = await withSpinner(
|
|
3444
3631
|
"Comparing local and remote resources...",
|
|
@@ -3482,7 +3669,8 @@ function prepareBodyForApi(type, body) {
|
|
|
3482
3669
|
if (Array.isArray(payload.tests)) {
|
|
3483
3670
|
payload.tests = payload.tests.map((t) => {
|
|
3484
3671
|
const match = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i.exec(t);
|
|
3485
|
-
|
|
3672
|
+
const id = match ? match[1] : t;
|
|
3673
|
+
return { id };
|
|
3486
3674
|
});
|
|
3487
3675
|
}
|
|
3488
3676
|
}
|
|
@@ -3545,7 +3733,7 @@ async function applyChange(client, change, opts) {
|
|
|
3545
3733
|
var deployCommand = new Command12("deploy").description("Push local config resources to the Supercheck project").option("--config <path>", "Path to config file").option("--dry-run", "Show what would change without applying").option("--force", "Skip confirmation prompt").option("--no-delete", "Do not delete remote resources missing from config").action(async (options) => {
|
|
3546
3734
|
const cwd = process.cwd();
|
|
3547
3735
|
const { config } = await loadConfig({ cwd, configPath: options.config });
|
|
3548
|
-
const client = createAuthenticatedClient();
|
|
3736
|
+
const client = createAuthenticatedClient(config.api?.baseUrl);
|
|
3549
3737
|
logger.newline();
|
|
3550
3738
|
logger.header("Deploying from config...");
|
|
3551
3739
|
logger.newline();
|
|
@@ -3584,6 +3772,14 @@ var deployCommand = new Command12("deploy").description("Push local config resou
|
|
|
3584
3772
|
const testsToValidate = actionable.filter((c) => c.type === "test" && (c.action === "create" || c.action === "update"));
|
|
3585
3773
|
if (testsToValidate.length > 0) {
|
|
3586
3774
|
await withSpinner("Validating test scripts...", async () => {
|
|
3775
|
+
for (const change of testsToValidate) {
|
|
3776
|
+
const script = String(change.local?.definition?.script ?? "");
|
|
3777
|
+
const declaredType = normalizeTestTypeForApi(change.local?.definition?.testType);
|
|
3778
|
+
const mismatch = validateScriptTypeMatch(script, declaredType);
|
|
3779
|
+
if (mismatch) {
|
|
3780
|
+
throw new CLIError(`${change.name}: ${mismatch}`, 3 /* ConfigError */);
|
|
3781
|
+
}
|
|
3782
|
+
}
|
|
3587
3783
|
const inputs = testsToValidate.map((change) => ({
|
|
3588
3784
|
name: change.name,
|
|
3589
3785
|
script: String(change.local?.definition?.script ?? ""),
|
|
@@ -3681,7 +3877,6 @@ async function fetchManagedResources(client, config) {
|
|
|
3681
3877
|
if (sp.id) managedIds.add(`statusPage:${sp.id}`);
|
|
3682
3878
|
}
|
|
3683
3879
|
}
|
|
3684
|
-
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
3685
3880
|
const patterns = {
|
|
3686
3881
|
playwright: config.tests?.playwright?.testMatch,
|
|
3687
3882
|
k6: config.tests?.k6?.testMatch
|
|
@@ -3690,9 +3885,9 @@ async function fetchManagedResources(client, config) {
|
|
|
3690
3885
|
const files = discoverFiles(cwd, patterns);
|
|
3691
3886
|
const testIds = /* @__PURE__ */ new Set();
|
|
3692
3887
|
for (const f of files) {
|
|
3693
|
-
const
|
|
3694
|
-
if (
|
|
3695
|
-
testIds.add(
|
|
3888
|
+
const uuid = extractUuidFromFilename(f.filename);
|
|
3889
|
+
if (uuid) {
|
|
3890
|
+
testIds.add(uuid);
|
|
3696
3891
|
}
|
|
3697
3892
|
}
|
|
3698
3893
|
try {
|
|
@@ -3768,7 +3963,7 @@ function logDestroyFetchError(resourceType, err) {
|
|
|
3768
3963
|
var destroyCommand = new Command13("destroy").description("Tear down managed resources from the Supercheck project").option("--config <path>", "Path to config file").option("--dry-run", "Show what would be destroyed without applying").option("--force", "Skip confirmation prompt").action(async (options) => {
|
|
3769
3964
|
const cwd = process.cwd();
|
|
3770
3965
|
const { config } = await loadConfig({ cwd, configPath: options.config });
|
|
3771
|
-
const client = createAuthenticatedClient();
|
|
3966
|
+
const client = createAuthenticatedClient(config.api?.baseUrl);
|
|
3772
3967
|
logger.newline();
|
|
3773
3968
|
const managed = await withSpinner(
|
|
3774
3969
|
"Scanning for managed resources...",
|
|
@@ -3801,7 +3996,7 @@ var destroyCommand = new Command13("destroy").description("Tear down managed res
|
|
|
3801
3996
|
logger.newline();
|
|
3802
3997
|
let succeeded = 0;
|
|
3803
3998
|
let failed = 0;
|
|
3804
|
-
const deleteOrder = ["
|
|
3999
|
+
const deleteOrder = ["statusPage", "job", "monitor", "variable", "tag", "test"];
|
|
3805
4000
|
const sorted = [...managed].sort((a, b) => deleteOrder.indexOf(a.type) - deleteOrder.indexOf(b.type));
|
|
3806
4001
|
for (const resource of sorted) {
|
|
3807
4002
|
try {
|
|
@@ -4010,6 +4205,7 @@ function buildMonitorDefinitions(monitors) {
|
|
|
4010
4205
|
if (m.config && typeof m.config === "object") {
|
|
4011
4206
|
const config = { ...m.config };
|
|
4012
4207
|
delete config.sslLastCheckedAt;
|
|
4208
|
+
delete config.aggregatedAlertState;
|
|
4013
4209
|
if (config.playwrightOptions && typeof config.playwrightOptions === "object") {
|
|
4014
4210
|
const opts = { ...config.playwrightOptions };
|
|
4015
4211
|
if (opts.retries === 0) delete opts.retries;
|
|
@@ -4280,8 +4476,8 @@ var pullCommand = new Command14("pull").description("Pull tests, monitors, jobs,
|
|
|
4280
4476
|
);
|
|
4281
4477
|
}
|
|
4282
4478
|
const cwd = process.cwd();
|
|
4283
|
-
const client = createAuthenticatedClient();
|
|
4284
4479
|
const existing = await tryLoadConfig({ cwd, configPath: options.config });
|
|
4480
|
+
const client = createAuthenticatedClient(existing?.config?.api?.baseUrl);
|
|
4285
4481
|
logger.newline();
|
|
4286
4482
|
logger.header("Pulling from Supercheck cloud...");
|
|
4287
4483
|
logger.newline();
|
|
@@ -4528,6 +4724,19 @@ var validateCommand = new Command15("validate").description("Validate local test
|
|
|
4528
4724
|
logger.warn("No local tests found to validate.");
|
|
4529
4725
|
return;
|
|
4530
4726
|
}
|
|
4727
|
+
let preflightFailed = false;
|
|
4728
|
+
for (const test of tests) {
|
|
4729
|
+
const script = String(test.definition?.script ?? "");
|
|
4730
|
+
const declaredType = normalizeTestTypeForApi(test.definition?.testType);
|
|
4731
|
+
const mismatch = validateScriptTypeMatch(script, declaredType);
|
|
4732
|
+
if (mismatch) {
|
|
4733
|
+
logger.error(`${pc8.red("\u2717")} ${test.name}: ${mismatch}`);
|
|
4734
|
+
preflightFailed = true;
|
|
4735
|
+
}
|
|
4736
|
+
}
|
|
4737
|
+
if (preflightFailed) {
|
|
4738
|
+
throw new CLIError("Script-type mismatch detected. Fix the test type or script content before validating.", 3 /* ConfigError */);
|
|
4739
|
+
}
|
|
4531
4740
|
const inputs = tests.map((test) => ({
|
|
4532
4741
|
name: test.name,
|
|
4533
4742
|
script: String(test.definition?.script ?? ""),
|
|
@@ -4614,7 +4823,7 @@ notificationCommand.command("create").description("Create a notification provide
|
|
|
4614
4823
|
logger.success(`Notification provider "${options.name}" created (${data.id})`);
|
|
4615
4824
|
outputDetail(data);
|
|
4616
4825
|
});
|
|
4617
|
-
notificationCommand.command("update <id>").description("Update a notification provider").option("--name <name>", "Provider name").option("--type <type>", "Provider type").option("--config <json>", "Provider config as JSON string").action(async (id, options) => {
|
|
4826
|
+
notificationCommand.command("update <id>").description("Update a notification provider").option("--name <name>", "Provider name").option("--type <type>", "Provider type").option("--config <json>", "Provider config as JSON string (replaces entire config \u2014 include all fields)").action(async (id, options) => {
|
|
4618
4827
|
const client = createAuthenticatedClient();
|
|
4619
4828
|
if (!options.name && !options.type && !options.config) {
|
|
4620
4829
|
logger.warn("No fields to update. Use --name, --type, or --config.");
|
|
@@ -4624,22 +4833,32 @@ notificationCommand.command("update <id>").description("Update a notification pr
|
|
|
4624
4833
|
"Fetching existing provider",
|
|
4625
4834
|
() => client.get(`/api/notification-providers/${id}`)
|
|
4626
4835
|
);
|
|
4627
|
-
|
|
4836
|
+
const maskedFields = existing.maskedFields ?? [];
|
|
4837
|
+
const updatedName = options.name ?? String(existing.name ?? "");
|
|
4838
|
+
const updatedType = options.type ?? String(existing.type ?? "");
|
|
4839
|
+
let updatedConfig;
|
|
4628
4840
|
if (options.config) {
|
|
4629
4841
|
try {
|
|
4630
|
-
updatedConfig =
|
|
4842
|
+
updatedConfig = JSON.parse(options.config);
|
|
4631
4843
|
} catch {
|
|
4632
4844
|
throw new CLIError("Invalid JSON in --config", 1 /* GeneralError */);
|
|
4633
4845
|
}
|
|
4846
|
+
} else if (maskedFields.length > 0) {
|
|
4847
|
+
logger.debug("Skipping config field (contains masked secrets that cannot be round-tripped safely)");
|
|
4848
|
+
updatedConfig = void 0;
|
|
4849
|
+
} else {
|
|
4850
|
+
updatedConfig = existing.config ?? {};
|
|
4851
|
+
}
|
|
4852
|
+
if (updatedConfig) {
|
|
4853
|
+
updatedConfig.name = updatedName;
|
|
4634
4854
|
}
|
|
4635
|
-
const updatedName = options.name ?? String(existing.name ?? "");
|
|
4636
|
-
const updatedType = options.type ?? String(existing.type ?? "");
|
|
4637
|
-
updatedConfig.name = updatedName;
|
|
4638
4855
|
const body = {
|
|
4639
4856
|
name: updatedName,
|
|
4640
|
-
type: updatedType
|
|
4641
|
-
config: updatedConfig
|
|
4857
|
+
type: updatedType
|
|
4642
4858
|
};
|
|
4859
|
+
if (updatedConfig) {
|
|
4860
|
+
body.config = updatedConfig;
|
|
4861
|
+
}
|
|
4643
4862
|
const { data } = await withSpinner(
|
|
4644
4863
|
"Updating notification provider",
|
|
4645
4864
|
() => client.put(`/api/notification-providers/${id}`, body)
|