@supercheck/cli 0.1.1-beta.1 → 0.1.1-beta.3
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 +58 -18
- package/dist/bin/supercheck.js +323 -129
- package/dist/bin/supercheck.js.map +1 -1
- package/package.json +3 -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-beta.
|
|
225
|
+
var CLI_VERSION = true ? "0.1.1-beta.3" : "0.0.0-dev";
|
|
220
226
|
|
|
221
227
|
// src/utils/proxy.ts
|
|
222
228
|
import { ProxyAgent } from "undici";
|
|
@@ -312,55 +318,29 @@ var ApiClient = class {
|
|
|
312
318
|
}
|
|
313
319
|
return url.toString();
|
|
314
320
|
}
|
|
315
|
-
getProxyEnv(url) {
|
|
316
|
-
if (this.proxy) return this.proxy;
|
|
317
|
-
const noProxyRaw = process.env.NO_PROXY ?? process.env.no_proxy;
|
|
318
|
-
if (noProxyRaw && this.isNoProxyMatch(url, noProxyRaw)) {
|
|
319
|
-
return null;
|
|
320
|
-
}
|
|
321
|
-
if (url.protocol === "https:") {
|
|
322
|
-
return process.env.HTTPS_PROXY ?? process.env.https_proxy ?? process.env.HTTP_PROXY ?? process.env.http_proxy ?? null;
|
|
323
|
-
}
|
|
324
|
-
if (url.protocol === "http:") {
|
|
325
|
-
return process.env.HTTP_PROXY ?? process.env.http_proxy ?? null;
|
|
326
|
-
}
|
|
327
|
-
return null;
|
|
328
|
-
}
|
|
329
|
-
isNoProxyMatch(url, noProxyRaw) {
|
|
330
|
-
const hostname = url.hostname;
|
|
331
|
-
const port = url.port || (url.protocol === "https:" ? "443" : "80");
|
|
332
|
-
const entries = noProxyRaw.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
333
|
-
if (entries.includes("*")) return true;
|
|
334
|
-
for (const entry of entries) {
|
|
335
|
-
const [hostPart, portPart] = entry.split(":");
|
|
336
|
-
const host = hostPart.trim();
|
|
337
|
-
const entryPort = portPart?.trim();
|
|
338
|
-
if (!host) continue;
|
|
339
|
-
if (entryPort && entryPort !== port) continue;
|
|
340
|
-
if (host.startsWith(".")) {
|
|
341
|
-
if (hostname.endsWith(host)) return true;
|
|
342
|
-
continue;
|
|
343
|
-
}
|
|
344
|
-
if (hostname === host) return true;
|
|
345
|
-
if (hostname.endsWith(`.${host}`)) return true;
|
|
346
|
-
}
|
|
347
|
-
return false;
|
|
348
|
-
}
|
|
349
321
|
/**
|
|
350
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
|
|
351
329
|
*/
|
|
352
330
|
async request(method, path, options) {
|
|
353
331
|
const url = this.buildUrl(path, options?.params);
|
|
354
332
|
const parsedUrl = new URL(url);
|
|
355
333
|
const headers = this.buildHeaders(options?.headers);
|
|
356
334
|
const maxRetries = options?.retries ?? MAX_RETRIES;
|
|
335
|
+
const upperMethod = method.toUpperCase();
|
|
336
|
+
const isIdempotent = ["GET", "PUT", "DELETE", "HEAD", "OPTIONS"].includes(upperMethod);
|
|
357
337
|
let lastError = null;
|
|
358
338
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
359
339
|
let timeoutId = null;
|
|
360
340
|
try {
|
|
361
341
|
const controller = new AbortController();
|
|
362
342
|
timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
363
|
-
const proxy = this.getProxyEnv(parsedUrl);
|
|
343
|
+
const proxy = this.proxy || getProxyEnv(parsedUrl);
|
|
364
344
|
const fetchOptions = {
|
|
365
345
|
method,
|
|
366
346
|
headers,
|
|
@@ -387,7 +367,7 @@ var ApiClient = class {
|
|
|
387
367
|
await this.sleep(waitMs);
|
|
388
368
|
continue;
|
|
389
369
|
}
|
|
390
|
-
if (response.status >= 500 && attempt < maxRetries) {
|
|
370
|
+
if (response.status >= 500 && attempt < maxRetries && isIdempotent) {
|
|
391
371
|
const waitMs = RETRY_BACKOFF_MS * Math.pow(2, attempt);
|
|
392
372
|
logger.debug(`Server error ${response.status}. Retrying in ${waitMs}ms...`);
|
|
393
373
|
await this.sleep(waitMs);
|
|
@@ -419,7 +399,7 @@ var ApiClient = class {
|
|
|
419
399
|
throw new TimeoutError(`Request timed out after ${this.timeout}ms: ${method} ${path}`);
|
|
420
400
|
}
|
|
421
401
|
lastError = err;
|
|
422
|
-
if (attempt < maxRetries) {
|
|
402
|
+
if (attempt < maxRetries && isIdempotent) {
|
|
423
403
|
const waitMs = RETRY_BACKOFF_MS * Math.pow(2, attempt);
|
|
424
404
|
logger.debug(`Network error: ${err.message}. Retrying in ${waitMs}ms...`);
|
|
425
405
|
await this.sleep(waitMs);
|
|
@@ -725,6 +705,7 @@ function normalizeAlertConfig(alert) {
|
|
|
725
705
|
function normalizeMonitorConfig(config) {
|
|
726
706
|
const normalized = { ...config };
|
|
727
707
|
delete normalized.sslLastCheckedAt;
|
|
708
|
+
delete normalized.aggregatedAlertState;
|
|
728
709
|
if (normalized.playwrightOptions && typeof normalized.playwrightOptions === "object") {
|
|
729
710
|
const opts = { ...normalized.playwrightOptions };
|
|
730
711
|
if (opts.retries === 0) delete opts.retries;
|
|
@@ -766,8 +747,8 @@ function normalizeRemoteRaw(type, raw) {
|
|
|
766
747
|
}
|
|
767
748
|
}
|
|
768
749
|
if (type === "test") {
|
|
769
|
-
if (normalized.type === "
|
|
770
|
-
else
|
|
750
|
+
if (normalized.type === "performance") normalized.testType = "k6";
|
|
751
|
+
else normalized.testType = "playwright";
|
|
771
752
|
delete normalized.type;
|
|
772
753
|
if (typeof normalized.script === "string") {
|
|
773
754
|
normalized.script = stripTitleMetadata(normalized.script);
|
|
@@ -1047,7 +1028,12 @@ function formatValue(value, column, row) {
|
|
|
1047
1028
|
return value ? pc2.green("\u2713") : pc2.red("\u2717");
|
|
1048
1029
|
}
|
|
1049
1030
|
if (value instanceof Date) return formatDate(value);
|
|
1050
|
-
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
|
+
}
|
|
1051
1037
|
if (typeof value === "number") {
|
|
1052
1038
|
const dateForNumber = maybeFormatDate(value, column?.key);
|
|
1053
1039
|
if (dateForNumber) return dateForNumber;
|
|
@@ -1078,6 +1064,22 @@ function maybeFormatDate(value, key) {
|
|
|
1078
1064
|
function formatDate(date) {
|
|
1079
1065
|
return date.toISOString().replace("T", " ").replace("Z", "");
|
|
1080
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
|
+
}
|
|
1081
1083
|
function outputDetail(data) {
|
|
1082
1084
|
if (currentFormat === "json") {
|
|
1083
1085
|
logger.output(JSON.stringify(data, null, 2));
|
|
@@ -1092,10 +1094,47 @@ function outputDetail(data) {
|
|
|
1092
1094
|
logger.info("No details available.");
|
|
1093
1095
|
return;
|
|
1094
1096
|
}
|
|
1095
|
-
const
|
|
1097
|
+
const scalarEntries = [];
|
|
1098
|
+
const nestedEntries = [];
|
|
1096
1099
|
for (const [key, value] of Object.entries(data)) {
|
|
1097
|
-
|
|
1098
|
-
|
|
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
|
+
}
|
|
1099
1138
|
}
|
|
1100
1139
|
}
|
|
1101
1140
|
|
|
@@ -1202,6 +1241,18 @@ var whoamiCommand = new Command("whoami").description("Show current authenticati
|
|
|
1202
1241
|
);
|
|
1203
1242
|
const tokenPreview = safeTokenPreview(token);
|
|
1204
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
|
+
}
|
|
1205
1256
|
logger.newline();
|
|
1206
1257
|
logger.header("Current Context");
|
|
1207
1258
|
logger.newline();
|
|
@@ -1439,7 +1490,9 @@ function validateNoSecrets(config) {
|
|
|
1439
1490
|
const secretPatterns = [
|
|
1440
1491
|
/sck_live_[a-zA-Z0-9]+/,
|
|
1441
1492
|
/sck_trigger_[a-zA-Z0-9]+/,
|
|
1442
|
-
/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,}/
|
|
1443
1496
|
];
|
|
1444
1497
|
for (const pattern of secretPatterns) {
|
|
1445
1498
|
if (pattern.test(configStr)) {
|
|
@@ -1453,13 +1506,10 @@ function validateNoSecrets(config) {
|
|
|
1453
1506
|
async function loadConfig(options = {}) {
|
|
1454
1507
|
const cwd = options.cwd ?? process.cwd();
|
|
1455
1508
|
const { config: loadEnv } = await import("dotenv");
|
|
1456
|
-
loadEnv({ path: resolve2(cwd, ".env") });
|
|
1509
|
+
loadEnv({ path: resolve2(cwd, ".env"), quiet: true });
|
|
1457
1510
|
const configPath = resolveConfigPath(cwd, options.configPath);
|
|
1458
1511
|
if (!configPath) {
|
|
1459
|
-
throw new
|
|
1460
|
-
"No supercheck.config.ts found. Run `supercheck init` to create one.",
|
|
1461
|
-
3 /* ConfigError */
|
|
1462
|
-
);
|
|
1512
|
+
throw new ConfigNotFoundError();
|
|
1463
1513
|
}
|
|
1464
1514
|
let config = await loadConfigFile(configPath);
|
|
1465
1515
|
const localConfigPath = resolveLocalConfigPath(cwd);
|
|
@@ -1487,7 +1537,7 @@ async function tryLoadConfig(options = {}) {
|
|
|
1487
1537
|
try {
|
|
1488
1538
|
return await loadConfig(options);
|
|
1489
1539
|
} catch (err) {
|
|
1490
|
-
if (err instanceof
|
|
1540
|
+
if (err instanceof ConfigNotFoundError) {
|
|
1491
1541
|
return null;
|
|
1492
1542
|
}
|
|
1493
1543
|
throw err;
|
|
@@ -1690,6 +1740,11 @@ function isNodeInstalled() {
|
|
|
1690
1740
|
}
|
|
1691
1741
|
async function installPlaywrightBrowsers(cwd, browser = "chromium") {
|
|
1692
1742
|
try {
|
|
1743
|
+
const allowedBrowsers = ["chromium", "firefox", "webkit", "chrome", "msedge"];
|
|
1744
|
+
if (!allowedBrowsers.includes(browser)) {
|
|
1745
|
+
logger.error(`Invalid browser: ${browser}. Allowed: ${allowedBrowsers.join(", ")}`);
|
|
1746
|
+
return false;
|
|
1747
|
+
}
|
|
1693
1748
|
logger.info(`Installing Playwright ${browser} browser...`);
|
|
1694
1749
|
execSync(`npx playwright install ${browser}`, {
|
|
1695
1750
|
cwd,
|
|
@@ -2034,9 +2089,10 @@ import { readFileSync as readFileSync3 } from "fs";
|
|
|
2034
2089
|
import { resolve as resolve6 } from "path";
|
|
2035
2090
|
|
|
2036
2091
|
// src/api/authenticated-client.ts
|
|
2037
|
-
function createAuthenticatedClient() {
|
|
2092
|
+
function createAuthenticatedClient(configBaseUrl) {
|
|
2038
2093
|
const token = requireAuth();
|
|
2039
|
-
const
|
|
2094
|
+
const storedBaseUrl = getStoredBaseUrl();
|
|
2095
|
+
const baseUrl = storedBaseUrl ?? configBaseUrl;
|
|
2040
2096
|
return getApiClient({ token, baseUrl: baseUrl ?? void 0 });
|
|
2041
2097
|
}
|
|
2042
2098
|
|
|
@@ -2065,6 +2121,21 @@ function parseIntStrict(value, name, opts) {
|
|
|
2065
2121
|
}
|
|
2066
2122
|
|
|
2067
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
|
+
}
|
|
2068
2139
|
function normalizeTestTypeForApi(localType) {
|
|
2069
2140
|
if (typeof localType !== "string") return void 0;
|
|
2070
2141
|
const normalized = localType.trim().toLowerCase();
|
|
@@ -2128,9 +2199,11 @@ async function validateScripts(client, inputs) {
|
|
|
2128
2199
|
import { spawn } from "child_process";
|
|
2129
2200
|
function runCommand(command, args, cwd) {
|
|
2130
2201
|
return new Promise((resolve10, reject) => {
|
|
2131
|
-
const
|
|
2202
|
+
const executable = process.platform === "win32" && command === "npx" ? "npx.cmd" : command;
|
|
2203
|
+
const child = spawn(executable, args, {
|
|
2132
2204
|
stdio: "inherit",
|
|
2133
|
-
shell:
|
|
2205
|
+
shell: false,
|
|
2206
|
+
// ✓ SECURE: Prevents command injection
|
|
2134
2207
|
cwd
|
|
2135
2208
|
});
|
|
2136
2209
|
child.on("error", (err) => reject(err));
|
|
@@ -2197,6 +2270,13 @@ function resolveJobLocalTests(cwd, tests, patterns) {
|
|
|
2197
2270
|
});
|
|
2198
2271
|
}
|
|
2199
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
|
+
}
|
|
2200
2280
|
const inputs = tests.map((test) => ({
|
|
2201
2281
|
name: test.name,
|
|
2202
2282
|
script: test.script,
|
|
@@ -2324,7 +2404,7 @@ jobCommand.command("get <id>").description("Get job details").action(async (id)
|
|
|
2324
2404
|
);
|
|
2325
2405
|
outputDetail(data);
|
|
2326
2406
|
});
|
|
2327
|
-
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) => {
|
|
2328
2408
|
const client = createAuthenticatedClient();
|
|
2329
2409
|
const tests = (options.tests ?? []).flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean);
|
|
2330
2410
|
if (tests.length === 0) {
|
|
@@ -2333,13 +2413,16 @@ jobCommand.command("create").description("Create a new job").requiredOption("--n
|
|
|
2333
2413
|
const body = {
|
|
2334
2414
|
name: options.name,
|
|
2335
2415
|
description: options.description,
|
|
2336
|
-
timeoutSeconds: parseIntStrict(options.timeout, "--timeout", { min: 1 }),
|
|
2337
|
-
retryCount: parseIntStrict(options.retries, "--retries", { min: 0 }),
|
|
2338
2416
|
config: {},
|
|
2339
2417
|
tests: tests.map((id) => ({ id }))
|
|
2340
2418
|
};
|
|
2341
2419
|
if (options.type) body.jobType = options.type;
|
|
2342
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
|
+
}
|
|
2343
2426
|
const { data } = await withSpinner(
|
|
2344
2427
|
"Creating job",
|
|
2345
2428
|
() => client.post("/api/jobs", body)
|
|
@@ -2349,17 +2432,20 @@ jobCommand.command("create").description("Create a new job").requiredOption("--n
|
|
|
2349
2432
|
logger.success(`Job "${options.name}" created (${jobId ?? "unknown"})`);
|
|
2350
2433
|
outputDetail(job);
|
|
2351
2434
|
});
|
|
2352
|
-
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) => {
|
|
2353
2436
|
const client = createAuthenticatedClient();
|
|
2354
2437
|
const body = {};
|
|
2355
2438
|
if (options.name !== void 0) body.name = options.name;
|
|
2356
2439
|
if (options.description !== void 0) body.description = options.description;
|
|
2357
2440
|
if (options.schedule !== void 0) body.cronSchedule = options.schedule;
|
|
2358
|
-
if (options.timeout !== void 0) body.timeoutSeconds = parseIntStrict(options.timeout, "--timeout", { min: 1 });
|
|
2359
|
-
if (options.retries !== void 0) body.retryCount = parseIntStrict(options.retries, "--retries", { min: 0 });
|
|
2360
2441
|
if (options.status !== void 0) body.status = options.status;
|
|
2361
2442
|
if (Object.keys(body).length === 0) {
|
|
2362
|
-
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));
|
|
2363
2449
|
return;
|
|
2364
2450
|
}
|
|
2365
2451
|
const { data } = await withSpinner(
|
|
@@ -2435,7 +2521,7 @@ jobCommand.command("run").description("Run a job immediately").requiredOption("-
|
|
|
2435
2521
|
"Running job",
|
|
2436
2522
|
() => client.post(
|
|
2437
2523
|
"/api/jobs/run",
|
|
2438
|
-
{ jobId: options.id, tests: payloadTests, trigger: "
|
|
2524
|
+
{ jobId: options.id, tests: payloadTests, trigger: "remote" }
|
|
2439
2525
|
)
|
|
2440
2526
|
);
|
|
2441
2527
|
logger.success(`Job started. Run ID: ${data.runId}`);
|
|
@@ -2721,6 +2807,13 @@ function collectLocalTests(cwd, patterns, options) {
|
|
|
2721
2807
|
});
|
|
2722
2808
|
}
|
|
2723
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
|
+
}
|
|
2724
2817
|
const inputs = tests.map((test) => ({
|
|
2725
2818
|
name: test.name,
|
|
2726
2819
|
script: test.script,
|
|
@@ -2800,7 +2893,7 @@ testCommand.command("get <id>").description("Get test details").option("--includ
|
|
|
2800
2893
|
);
|
|
2801
2894
|
outputDetail(data);
|
|
2802
2895
|
});
|
|
2803
|
-
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) => {
|
|
2804
2897
|
const { readFileSync: readFileSync6 } = await import("fs");
|
|
2805
2898
|
const { resolve: resolve10 } = await import("path");
|
|
2806
2899
|
const filePath = resolve10(process.cwd(), options.file);
|
|
@@ -2811,7 +2904,10 @@ testCommand.command("create").description("Create a new test").requiredOption("-
|
|
|
2811
2904
|
throw new CLIError(`Cannot read file: ${filePath}`, 1 /* GeneralError */);
|
|
2812
2905
|
}
|
|
2813
2906
|
const typeArg = options.type || inferTestType(options.file) || "playwright";
|
|
2814
|
-
const
|
|
2907
|
+
const typeMismatchError = validateScriptTypeMatch(script, normalizeTestTypeForApi(typeArg));
|
|
2908
|
+
if (typeMismatchError) {
|
|
2909
|
+
throw new CLIError(typeMismatchError, 3 /* ConfigError */);
|
|
2910
|
+
}
|
|
2815
2911
|
const encodedScript = Buffer2.from(script, "utf-8").toString("base64");
|
|
2816
2912
|
const body = {
|
|
2817
2913
|
title: options.title,
|
|
@@ -2819,6 +2915,13 @@ testCommand.command("create").description("Create a new test").requiredOption("-
|
|
|
2819
2915
|
type: normalizeTestType(typeArg)
|
|
2820
2916
|
};
|
|
2821
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();
|
|
2822
2925
|
const { data } = await withSpinner(
|
|
2823
2926
|
"Creating test",
|
|
2824
2927
|
() => client.post("/api/tests", body)
|
|
@@ -2827,7 +2930,7 @@ testCommand.command("create").description("Create a new test").requiredOption("-
|
|
|
2827
2930
|
logger.success(`Test "${options.title}" created${createdId ? ` (${createdId})` : ""}`);
|
|
2828
2931
|
outputDetail(data);
|
|
2829
2932
|
});
|
|
2830
|
-
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) => {
|
|
2831
2934
|
const client = createAuthenticatedClient();
|
|
2832
2935
|
const body = {};
|
|
2833
2936
|
if (options.title !== void 0) body.title = options.title;
|
|
@@ -2847,6 +2950,15 @@ testCommand.command("update <id>").description("Update a test").option("--title
|
|
|
2847
2950
|
logger.warn("No fields to update. Use --title, --file, or --description.");
|
|
2848
2951
|
return;
|
|
2849
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
|
+
}
|
|
2850
2962
|
const { data } = await withSpinner(
|
|
2851
2963
|
"Updating test",
|
|
2852
2964
|
() => client.patch(`/api/tests/${id}`, body)
|
|
@@ -2870,25 +2982,7 @@ testCommand.command("delete <id>").description("Delete a test").option("--force"
|
|
|
2870
2982
|
);
|
|
2871
2983
|
logger.success(`Test ${id} deleted`);
|
|
2872
2984
|
});
|
|
2873
|
-
testCommand.command("run").description("Run tests locally
|
|
2874
|
-
if (options.local && options.cloud) {
|
|
2875
|
-
throw new CLIError("--local and --cloud are mutually exclusive.", 3 /* ConfigError */);
|
|
2876
|
-
}
|
|
2877
|
-
const useCloud = options.cloud || !options.local && !!options.id;
|
|
2878
|
-
if (useCloud) {
|
|
2879
|
-
if (!options.id) {
|
|
2880
|
-
throw new CLIError("Cloud runs require --id <testId>.", 3 /* ConfigError */);
|
|
2881
|
-
}
|
|
2882
|
-
const client2 = createAuthenticatedClient();
|
|
2883
|
-
const body = {};
|
|
2884
|
-
if (options.location) body.location = options.location;
|
|
2885
|
-
const { data } = await withSpinner(
|
|
2886
|
-
"Executing test",
|
|
2887
|
-
() => client2.post(`/api/tests/${options.id}/execute`, body)
|
|
2888
|
-
);
|
|
2889
|
-
outputDetail(data);
|
|
2890
|
-
return;
|
|
2891
|
-
}
|
|
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) => {
|
|
2892
2986
|
if (!options.file && !options.all) {
|
|
2893
2987
|
throw new CLIError("Local runs require --file <path> or --all.", 3 /* ConfigError */);
|
|
2894
2988
|
}
|
|
@@ -2935,16 +3029,6 @@ testCommand.command("run").description("Run tests locally or in the cloud").opti
|
|
|
2935
3029
|
await runK6Tests(k6Tests, cwd);
|
|
2936
3030
|
}
|
|
2937
3031
|
});
|
|
2938
|
-
testCommand.command("execute <id>").description("Execute a test immediately").option("--location <location>", "Execution location (k6 only)").action(async (id, options) => {
|
|
2939
|
-
const client = createAuthenticatedClient();
|
|
2940
|
-
const body = {};
|
|
2941
|
-
if (options.location) body.location = options.location;
|
|
2942
|
-
const { data } = await withSpinner(
|
|
2943
|
-
"Executing test",
|
|
2944
|
-
() => client.post(`/api/tests/${id}/execute`, body)
|
|
2945
|
-
);
|
|
2946
|
-
outputDetail(data);
|
|
2947
|
-
});
|
|
2948
3032
|
testCommand.command("tags <id>").description("Get test tags").action(async (id) => {
|
|
2949
3033
|
const client = createAuthenticatedClient();
|
|
2950
3034
|
const { data } = await withSpinner(
|
|
@@ -2969,6 +3053,11 @@ testCommand.command("validate").description("Validate a test script").requiredOp
|
|
|
2969
3053
|
throw new CLIError(`Cannot read file: ${filePath}`, 1 /* GeneralError */);
|
|
2970
3054
|
}
|
|
2971
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
|
+
}
|
|
2972
3061
|
const client = createAuthenticatedClient();
|
|
2973
3062
|
const results = await withSpinner(
|
|
2974
3063
|
"Validating script",
|
|
@@ -3097,23 +3186,29 @@ monitorCommand.command("get <id>").description("Get monitor details").action(asy
|
|
|
3097
3186
|
);
|
|
3098
3187
|
outputDetail(data);
|
|
3099
3188
|
});
|
|
3100
|
-
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) => {
|
|
3101
3190
|
const client = createAuthenticatedClient();
|
|
3102
3191
|
const { data } = await withSpinner(
|
|
3103
3192
|
"Fetching monitor results",
|
|
3104
3193
|
() => client.get(
|
|
3105
3194
|
`/api/monitors/${id}/results`,
|
|
3106
|
-
{ limit: options.limit }
|
|
3195
|
+
{ limit: options.limit, page: options.page }
|
|
3107
3196
|
)
|
|
3108
3197
|
);
|
|
3109
|
-
output(data.
|
|
3198
|
+
output(data.data, {
|
|
3110
3199
|
columns: [
|
|
3111
|
-
{ key: "
|
|
3200
|
+
{ key: "checkedAt", header: "Time" },
|
|
3112
3201
|
{ key: "status", header: "Status" },
|
|
3113
3202
|
{ key: "responseTime", header: "Response (ms)" },
|
|
3114
3203
|
{ key: "location", header: "Location" }
|
|
3115
3204
|
]
|
|
3116
3205
|
});
|
|
3206
|
+
if (data.pagination) {
|
|
3207
|
+
logger.info(
|
|
3208
|
+
`
|
|
3209
|
+
Page ${data.pagination.page}/${data.pagination.totalPages} (${data.pagination.total} total)`
|
|
3210
|
+
);
|
|
3211
|
+
}
|
|
3117
3212
|
});
|
|
3118
3213
|
monitorCommand.command("stats <id>").description("Get monitor performance statistics").action(async (id) => {
|
|
3119
3214
|
const client = createAuthenticatedClient();
|
|
@@ -3121,7 +3216,28 @@ monitorCommand.command("stats <id>").description("Get monitor performance statis
|
|
|
3121
3216
|
"Fetching monitor statistics",
|
|
3122
3217
|
() => client.get(`/api/monitors/${id}/stats`)
|
|
3123
3218
|
);
|
|
3124
|
-
|
|
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
|
+
}
|
|
3125
3241
|
});
|
|
3126
3242
|
monitorCommand.command("status <id>").description("Get current monitor status").action(async (id) => {
|
|
3127
3243
|
const client = createAuthenticatedClient();
|
|
@@ -3138,10 +3254,22 @@ monitorCommand.command("status <id>").description("Get current monitor status").
|
|
|
3138
3254
|
};
|
|
3139
3255
|
outputDetail(statusInfo);
|
|
3140
3256
|
});
|
|
3141
|
-
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) => {
|
|
3142
3258
|
const client = createAuthenticatedClient();
|
|
3143
|
-
|
|
3144
|
-
|
|
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
|
+
}
|
|
3145
3273
|
const body = {
|
|
3146
3274
|
name: options.name,
|
|
3147
3275
|
target: options.url,
|
|
@@ -3153,6 +3281,11 @@ monitorCommand.command("create").description("Create a new monitor").requiredOpt
|
|
|
3153
3281
|
method: options.method
|
|
3154
3282
|
}
|
|
3155
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
|
+
}
|
|
3156
3289
|
const { data } = await withSpinner(
|
|
3157
3290
|
"Creating monitor",
|
|
3158
3291
|
() => client.post("/api/monitors", body)
|
|
@@ -3160,14 +3293,23 @@ monitorCommand.command("create").description("Create a new monitor").requiredOpt
|
|
|
3160
3293
|
logger.success(`Monitor "${options.name}" created (${data.id})`);
|
|
3161
3294
|
outputDetail(data);
|
|
3162
3295
|
});
|
|
3163
|
-
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) => {
|
|
3164
3297
|
const client = createAuthenticatedClient();
|
|
3165
3298
|
const body = {};
|
|
3166
3299
|
if (options.name !== void 0) body.name = options.name;
|
|
3167
3300
|
if (options.url !== void 0) body.target = options.url;
|
|
3168
3301
|
if (options.interval !== void 0) {
|
|
3169
|
-
|
|
3170
|
-
|
|
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 });
|
|
3171
3313
|
}
|
|
3172
3314
|
if (options.active !== void 0) body.enabled = options.active === "true";
|
|
3173
3315
|
const config = {};
|
|
@@ -3175,7 +3317,12 @@ monitorCommand.command("update <id>").description("Update a monitor").option("--
|
|
|
3175
3317
|
if (options.method !== void 0) config.method = options.method;
|
|
3176
3318
|
if (Object.keys(config).length > 0) body.config = config;
|
|
3177
3319
|
if (Object.keys(body).length === 0) {
|
|
3178
|
-
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));
|
|
3179
3326
|
return;
|
|
3180
3327
|
}
|
|
3181
3328
|
const { data } = await withSpinner(
|
|
@@ -3478,7 +3625,7 @@ function formatChangePlan(changes) {
|
|
|
3478
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) => {
|
|
3479
3626
|
const cwd = process.cwd();
|
|
3480
3627
|
const { config } = await loadConfig({ cwd, configPath: options.config });
|
|
3481
|
-
const client = createAuthenticatedClient();
|
|
3628
|
+
const client = createAuthenticatedClient(config.api?.baseUrl);
|
|
3482
3629
|
logger.newline();
|
|
3483
3630
|
const { localResources, remoteResources } = await withSpinner(
|
|
3484
3631
|
"Comparing local and remote resources...",
|
|
@@ -3522,7 +3669,8 @@ function prepareBodyForApi(type, body) {
|
|
|
3522
3669
|
if (Array.isArray(payload.tests)) {
|
|
3523
3670
|
payload.tests = payload.tests.map((t) => {
|
|
3524
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);
|
|
3525
|
-
|
|
3672
|
+
const id = match ? match[1] : t;
|
|
3673
|
+
return { id };
|
|
3526
3674
|
});
|
|
3527
3675
|
}
|
|
3528
3676
|
}
|
|
@@ -3539,7 +3687,8 @@ async function applyChange(client, change, opts) {
|
|
|
3539
3687
|
const endpoint = getApiEndpoint(change.type);
|
|
3540
3688
|
switch (change.action) {
|
|
3541
3689
|
case "create": {
|
|
3542
|
-
const
|
|
3690
|
+
const rawBody = { ...change.local.definition };
|
|
3691
|
+
delete rawBody.id;
|
|
3543
3692
|
const body = prepareBodyForApi(change.type, rawBody);
|
|
3544
3693
|
const response = await client.post(endpoint, body);
|
|
3545
3694
|
if (change.type === "test") {
|
|
@@ -3562,7 +3711,8 @@ async function applyChange(client, change, opts) {
|
|
|
3562
3711
|
}
|
|
3563
3712
|
case "update": {
|
|
3564
3713
|
const id = change.id ?? change.remote.id;
|
|
3565
|
-
const
|
|
3714
|
+
const rawBody = { ...change.local.definition };
|
|
3715
|
+
delete rawBody.id;
|
|
3566
3716
|
const body = prepareBodyForApi(change.type, rawBody);
|
|
3567
3717
|
await client.put(`${endpoint}/${id}`, body);
|
|
3568
3718
|
return { success: true };
|
|
@@ -3583,7 +3733,7 @@ async function applyChange(client, change, opts) {
|
|
|
3583
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) => {
|
|
3584
3734
|
const cwd = process.cwd();
|
|
3585
3735
|
const { config } = await loadConfig({ cwd, configPath: options.config });
|
|
3586
|
-
const client = createAuthenticatedClient();
|
|
3736
|
+
const client = createAuthenticatedClient(config.api?.baseUrl);
|
|
3587
3737
|
logger.newline();
|
|
3588
3738
|
logger.header("Deploying from config...");
|
|
3589
3739
|
logger.newline();
|
|
@@ -3601,6 +3751,15 @@ var deployCommand = new Command12("deploy").description("Push local config resou
|
|
|
3601
3751
|
changes = changes.filter((c) => c.action !== "delete");
|
|
3602
3752
|
}
|
|
3603
3753
|
const actionable = changes.filter((c) => c.action !== "no-change");
|
|
3754
|
+
const unsupportedStatusPageMutations = actionable.filter(
|
|
3755
|
+
(c) => c.type === "statusPage" && (c.action === "create" || c.action === "update")
|
|
3756
|
+
);
|
|
3757
|
+
if (unsupportedStatusPageMutations.length > 0) {
|
|
3758
|
+
throw new CLIError(
|
|
3759
|
+
"Status page create/update is not supported by the current API. Remove statusPages create/update changes from config (or apply them in the dashboard) and run deploy again.",
|
|
3760
|
+
3 /* ConfigError */
|
|
3761
|
+
);
|
|
3762
|
+
}
|
|
3604
3763
|
if (actionable.length === 0) {
|
|
3605
3764
|
logger.success("No changes to deploy. Everything is in sync.");
|
|
3606
3765
|
return;
|
|
@@ -3613,6 +3772,14 @@ var deployCommand = new Command12("deploy").description("Push local config resou
|
|
|
3613
3772
|
const testsToValidate = actionable.filter((c) => c.type === "test" && (c.action === "create" || c.action === "update"));
|
|
3614
3773
|
if (testsToValidate.length > 0) {
|
|
3615
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
|
+
}
|
|
3616
3783
|
const inputs = testsToValidate.map((change) => ({
|
|
3617
3784
|
name: change.name,
|
|
3618
3785
|
script: String(change.local?.definition?.script ?? ""),
|
|
@@ -3710,7 +3877,6 @@ async function fetchManagedResources(client, config) {
|
|
|
3710
3877
|
if (sp.id) managedIds.add(`statusPage:${sp.id}`);
|
|
3711
3878
|
}
|
|
3712
3879
|
}
|
|
3713
|
-
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
3714
3880
|
const patterns = {
|
|
3715
3881
|
playwright: config.tests?.playwright?.testMatch,
|
|
3716
3882
|
k6: config.tests?.k6?.testMatch
|
|
@@ -3719,9 +3885,9 @@ async function fetchManagedResources(client, config) {
|
|
|
3719
3885
|
const files = discoverFiles(cwd, patterns);
|
|
3720
3886
|
const testIds = /* @__PURE__ */ new Set();
|
|
3721
3887
|
for (const f of files) {
|
|
3722
|
-
const
|
|
3723
|
-
if (
|
|
3724
|
-
testIds.add(
|
|
3888
|
+
const uuid = extractUuidFromFilename(f.filename);
|
|
3889
|
+
if (uuid) {
|
|
3890
|
+
testIds.add(uuid);
|
|
3725
3891
|
}
|
|
3726
3892
|
}
|
|
3727
3893
|
try {
|
|
@@ -3797,7 +3963,7 @@ function logDestroyFetchError(resourceType, err) {
|
|
|
3797
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) => {
|
|
3798
3964
|
const cwd = process.cwd();
|
|
3799
3965
|
const { config } = await loadConfig({ cwd, configPath: options.config });
|
|
3800
|
-
const client = createAuthenticatedClient();
|
|
3966
|
+
const client = createAuthenticatedClient(config.api?.baseUrl);
|
|
3801
3967
|
logger.newline();
|
|
3802
3968
|
const managed = await withSpinner(
|
|
3803
3969
|
"Scanning for managed resources...",
|
|
@@ -3830,7 +3996,7 @@ var destroyCommand = new Command13("destroy").description("Tear down managed res
|
|
|
3830
3996
|
logger.newline();
|
|
3831
3997
|
let succeeded = 0;
|
|
3832
3998
|
let failed = 0;
|
|
3833
|
-
const deleteOrder = ["
|
|
3999
|
+
const deleteOrder = ["statusPage", "job", "monitor", "variable", "tag", "test"];
|
|
3834
4000
|
const sorted = [...managed].sort((a, b) => deleteOrder.indexOf(a.type) - deleteOrder.indexOf(b.type));
|
|
3835
4001
|
for (const resource of sorted) {
|
|
3836
4002
|
try {
|
|
@@ -3885,7 +4051,11 @@ function decodeScript(script) {
|
|
|
3885
4051
|
if (reEncoded !== trimmed) {
|
|
3886
4052
|
return script;
|
|
3887
4053
|
}
|
|
3888
|
-
|
|
4054
|
+
const isTextLike = decoded.length > 0 && Array.from(decoded).every((char) => {
|
|
4055
|
+
const code = char.codePointAt(0) ?? 0;
|
|
4056
|
+
return code === 9 || code === 10 || code === 13 || code >= 32;
|
|
4057
|
+
});
|
|
4058
|
+
if (isTextLike) {
|
|
3889
4059
|
return decoded;
|
|
3890
4060
|
}
|
|
3891
4061
|
} catch {
|
|
@@ -4035,6 +4205,7 @@ function buildMonitorDefinitions(monitors) {
|
|
|
4035
4205
|
if (m.config && typeof m.config === "object") {
|
|
4036
4206
|
const config = { ...m.config };
|
|
4037
4207
|
delete config.sslLastCheckedAt;
|
|
4208
|
+
delete config.aggregatedAlertState;
|
|
4038
4209
|
if (config.playwrightOptions && typeof config.playwrightOptions === "object") {
|
|
4039
4210
|
const opts = { ...config.playwrightOptions };
|
|
4040
4211
|
if (opts.retries === 0) delete opts.retries;
|
|
@@ -4228,7 +4399,7 @@ function generateConfigContent(opts) {
|
|
|
4228
4399
|
parts.push(" *");
|
|
4229
4400
|
parts.push(" * supercheck test list List all tests");
|
|
4230
4401
|
parts.push(" * supercheck test validate Validate a local test script");
|
|
4231
|
-
parts.push(" * supercheck test
|
|
4402
|
+
parts.push(" * supercheck test run --local Run local test scripts");
|
|
4232
4403
|
parts.push(" *");
|
|
4233
4404
|
parts.push(" * supercheck monitor list List all monitors");
|
|
4234
4405
|
parts.push(" * supercheck job list List all jobs");
|
|
@@ -4305,8 +4476,8 @@ var pullCommand = new Command14("pull").description("Pull tests, monitors, jobs,
|
|
|
4305
4476
|
);
|
|
4306
4477
|
}
|
|
4307
4478
|
const cwd = process.cwd();
|
|
4308
|
-
const client = createAuthenticatedClient();
|
|
4309
4479
|
const existing = await tryLoadConfig({ cwd, configPath: options.config });
|
|
4480
|
+
const client = createAuthenticatedClient(existing?.config?.api?.baseUrl);
|
|
4310
4481
|
logger.newline();
|
|
4311
4482
|
logger.header("Pulling from Supercheck cloud...");
|
|
4312
4483
|
logger.newline();
|
|
@@ -4553,6 +4724,19 @@ var validateCommand = new Command15("validate").description("Validate local test
|
|
|
4553
4724
|
logger.warn("No local tests found to validate.");
|
|
4554
4725
|
return;
|
|
4555
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
|
+
}
|
|
4556
4740
|
const inputs = tests.map((test) => ({
|
|
4557
4741
|
name: test.name,
|
|
4558
4742
|
script: String(test.definition?.script ?? ""),
|
|
@@ -4639,7 +4823,7 @@ notificationCommand.command("create").description("Create a notification provide
|
|
|
4639
4823
|
logger.success(`Notification provider "${options.name}" created (${data.id})`);
|
|
4640
4824
|
outputDetail(data);
|
|
4641
4825
|
});
|
|
4642
|
-
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) => {
|
|
4643
4827
|
const client = createAuthenticatedClient();
|
|
4644
4828
|
if (!options.name && !options.type && !options.config) {
|
|
4645
4829
|
logger.warn("No fields to update. Use --name, --type, or --config.");
|
|
@@ -4649,22 +4833,32 @@ notificationCommand.command("update <id>").description("Update a notification pr
|
|
|
4649
4833
|
"Fetching existing provider",
|
|
4650
4834
|
() => client.get(`/api/notification-providers/${id}`)
|
|
4651
4835
|
);
|
|
4652
|
-
|
|
4836
|
+
const maskedFields = existing.maskedFields ?? [];
|
|
4837
|
+
const updatedName = options.name ?? String(existing.name ?? "");
|
|
4838
|
+
const updatedType = options.type ?? String(existing.type ?? "");
|
|
4839
|
+
let updatedConfig;
|
|
4653
4840
|
if (options.config) {
|
|
4654
4841
|
try {
|
|
4655
|
-
updatedConfig =
|
|
4842
|
+
updatedConfig = JSON.parse(options.config);
|
|
4656
4843
|
} catch {
|
|
4657
4844
|
throw new CLIError("Invalid JSON in --config", 1 /* GeneralError */);
|
|
4658
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;
|
|
4659
4854
|
}
|
|
4660
|
-
const updatedName = options.name ?? String(existing.name ?? "");
|
|
4661
|
-
const updatedType = options.type ?? String(existing.type ?? "");
|
|
4662
|
-
updatedConfig.name = updatedName;
|
|
4663
4855
|
const body = {
|
|
4664
4856
|
name: updatedName,
|
|
4665
|
-
type: updatedType
|
|
4666
|
-
config: updatedConfig
|
|
4857
|
+
type: updatedType
|
|
4667
4858
|
};
|
|
4859
|
+
if (updatedConfig) {
|
|
4860
|
+
body.config = updatedConfig;
|
|
4861
|
+
}
|
|
4668
4862
|
const { data } = await withSpinner(
|
|
4669
4863
|
"Updating notification provider",
|
|
4670
4864
|
() => client.put(`/api/notification-providers/${id}`, body)
|