@supercheck/cli 0.1.0-beta.9 → 0.1.1-beta.2
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 +72 -9
- package/dist/bin/supercheck.js +1315 -323
- package/dist/bin/supercheck.js.map +1 -1
- package/package.json +5 -3
package/dist/bin/supercheck.js
CHANGED
|
@@ -4,8 +4,8 @@ import {
|
|
|
4
4
|
} from "../chunk-W54DX2I7.js";
|
|
5
5
|
|
|
6
6
|
// src/bin/supercheck.ts
|
|
7
|
-
import { Command as
|
|
8
|
-
import
|
|
7
|
+
import { Command as Command21 } from "commander";
|
|
8
|
+
import pc10 from "picocolors";
|
|
9
9
|
|
|
10
10
|
// src/commands/login.ts
|
|
11
11
|
import { Command } from "commander";
|
|
@@ -193,6 +193,9 @@ function getStoredOrganization() {
|
|
|
193
193
|
function getStoredProject() {
|
|
194
194
|
return process.env.SUPERCHECK_PROJECT ?? store.get("project") ?? null;
|
|
195
195
|
}
|
|
196
|
+
function isAuthenticated() {
|
|
197
|
+
return getToken() !== null;
|
|
198
|
+
}
|
|
196
199
|
function requireAuth() {
|
|
197
200
|
const token = getToken();
|
|
198
201
|
if (!token) {
|
|
@@ -213,14 +216,10 @@ function requireTriggerKey() {
|
|
|
213
216
|
}
|
|
214
217
|
|
|
215
218
|
// src/version.ts
|
|
216
|
-
var CLI_VERSION = true ? "0.1.
|
|
219
|
+
var CLI_VERSION = true ? "0.1.1-beta.2" : "0.0.0-dev";
|
|
217
220
|
|
|
218
|
-
// src/
|
|
221
|
+
// src/utils/proxy.ts
|
|
219
222
|
import { ProxyAgent } from "undici";
|
|
220
|
-
var DEFAULT_BASE_URL = "https://app.supercheck.io";
|
|
221
|
-
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
222
|
-
var MAX_RETRIES = 3;
|
|
223
|
-
var RETRY_BACKOFF_MS = 1e3;
|
|
224
223
|
var proxyAgents = /* @__PURE__ */ new Map();
|
|
225
224
|
function getProxyAgent(proxyUrl) {
|
|
226
225
|
const existing = proxyAgents.get(proxyUrl);
|
|
@@ -229,6 +228,45 @@ function getProxyAgent(proxyUrl) {
|
|
|
229
228
|
proxyAgents.set(proxyUrl, created);
|
|
230
229
|
return created;
|
|
231
230
|
}
|
|
231
|
+
function isNoProxyMatch(url, noProxyRaw) {
|
|
232
|
+
const hostname = url.hostname;
|
|
233
|
+
const port = url.port || (url.protocol === "https:" ? "443" : "80");
|
|
234
|
+
const entries = noProxyRaw.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
235
|
+
if (entries.includes("*")) return true;
|
|
236
|
+
for (const entry of entries) {
|
|
237
|
+
const [hostPart, portPart] = entry.split(":");
|
|
238
|
+
const host = hostPart.trim();
|
|
239
|
+
const entryPort = portPart?.trim();
|
|
240
|
+
if (!host) continue;
|
|
241
|
+
if (entryPort && entryPort !== port) continue;
|
|
242
|
+
if (host.startsWith(".")) {
|
|
243
|
+
if (hostname.endsWith(host)) return true;
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (hostname === host) return true;
|
|
247
|
+
if (hostname.endsWith(`.${host}`)) return true;
|
|
248
|
+
}
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
function getProxyEnv(url) {
|
|
252
|
+
const noProxyRaw = process.env.NO_PROXY ?? process.env.no_proxy;
|
|
253
|
+
if (noProxyRaw && isNoProxyMatch(url, noProxyRaw)) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
if (url.protocol === "https:") {
|
|
257
|
+
return process.env.HTTPS_PROXY ?? process.env.https_proxy ?? process.env.HTTP_PROXY ?? process.env.http_proxy ?? null;
|
|
258
|
+
}
|
|
259
|
+
if (url.protocol === "http:") {
|
|
260
|
+
return process.env.HTTP_PROXY ?? process.env.http_proxy ?? null;
|
|
261
|
+
}
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// src/api/client.ts
|
|
266
|
+
var DEFAULT_BASE_URL = "https://app.supercheck.io";
|
|
267
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
268
|
+
var MAX_RETRIES = 3;
|
|
269
|
+
var RETRY_BACKOFF_MS = 1e3;
|
|
232
270
|
var ApiClient = class {
|
|
233
271
|
baseUrl;
|
|
234
272
|
token;
|
|
@@ -274,40 +312,6 @@ var ApiClient = class {
|
|
|
274
312
|
}
|
|
275
313
|
return url.toString();
|
|
276
314
|
}
|
|
277
|
-
getProxyEnv(url) {
|
|
278
|
-
if (this.proxy) return this.proxy;
|
|
279
|
-
const noProxyRaw = process.env.NO_PROXY ?? process.env.no_proxy;
|
|
280
|
-
if (noProxyRaw && this.isNoProxyMatch(url, noProxyRaw)) {
|
|
281
|
-
return null;
|
|
282
|
-
}
|
|
283
|
-
if (url.protocol === "https:") {
|
|
284
|
-
return process.env.HTTPS_PROXY ?? process.env.https_proxy ?? process.env.HTTP_PROXY ?? process.env.http_proxy ?? null;
|
|
285
|
-
}
|
|
286
|
-
if (url.protocol === "http:") {
|
|
287
|
-
return process.env.HTTP_PROXY ?? process.env.http_proxy ?? null;
|
|
288
|
-
}
|
|
289
|
-
return null;
|
|
290
|
-
}
|
|
291
|
-
isNoProxyMatch(url, noProxyRaw) {
|
|
292
|
-
const hostname = url.hostname;
|
|
293
|
-
const port = url.port || (url.protocol === "https:" ? "443" : "80");
|
|
294
|
-
const entries = noProxyRaw.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
295
|
-
if (entries.includes("*")) return true;
|
|
296
|
-
for (const entry of entries) {
|
|
297
|
-
const [hostPart, portPart] = entry.split(":");
|
|
298
|
-
const host = hostPart.trim();
|
|
299
|
-
const entryPort = portPart?.trim();
|
|
300
|
-
if (!host) continue;
|
|
301
|
-
if (entryPort && entryPort !== port) continue;
|
|
302
|
-
if (host.startsWith(".")) {
|
|
303
|
-
if (hostname.endsWith(host)) return true;
|
|
304
|
-
continue;
|
|
305
|
-
}
|
|
306
|
-
if (hostname === host) return true;
|
|
307
|
-
if (hostname.endsWith(`.${host}`)) return true;
|
|
308
|
-
}
|
|
309
|
-
return false;
|
|
310
|
-
}
|
|
311
315
|
/**
|
|
312
316
|
* Execute an HTTP request with retries, timeout, and rate limit handling.
|
|
313
317
|
*/
|
|
@@ -322,7 +326,7 @@ var ApiClient = class {
|
|
|
322
326
|
try {
|
|
323
327
|
const controller = new AbortController();
|
|
324
328
|
timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
325
|
-
const proxy = this.getProxyEnv(parsedUrl);
|
|
329
|
+
const proxy = this.proxy || getProxyEnv(parsedUrl);
|
|
326
330
|
const fetchOptions = {
|
|
327
331
|
method,
|
|
328
332
|
headers,
|
|
@@ -410,7 +414,7 @@ var ApiClient = class {
|
|
|
410
414
|
return this.request("DELETE", path);
|
|
411
415
|
}
|
|
412
416
|
sleep(ms) {
|
|
413
|
-
return new Promise((
|
|
417
|
+
return new Promise((resolve10) => setTimeout(resolve10, ms));
|
|
414
418
|
}
|
|
415
419
|
};
|
|
416
420
|
var defaultClient = null;
|
|
@@ -430,9 +434,9 @@ function getApiClient(options) {
|
|
|
430
434
|
import { readdirSync, statSync, readFileSync } from "fs";
|
|
431
435
|
import { resolve, relative, basename } from "path";
|
|
432
436
|
function matchGlob(filePath, pattern) {
|
|
433
|
-
const withPlaceholders = pattern.replace(/\*\*/g, "
|
|
437
|
+
const withPlaceholders = pattern.replace(/\/\*\*\//g, "__GLOBSTAR_SLASH__").replace(/\*\*/g, "__GLOBSTAR__").replace(/\*/g, "__STAR__").replace(/\?/g, "__QMARK__");
|
|
434
438
|
const escaped = withPlaceholders.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
435
|
-
const regexStr = escaped.replace(
|
|
439
|
+
const regexStr = escaped.replace(/__GLOBSTAR_SLASH__/g, "(?:/|/.+/)").replace(/__GLOBSTAR__/g, ".*").replace(/__STAR__/g, "[^/]*").replace(/__QMARK__/g, ".");
|
|
436
440
|
return new RegExp(`^${regexStr}$`).test(filePath);
|
|
437
441
|
}
|
|
438
442
|
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
@@ -526,8 +530,29 @@ function readFileContent(filePath) {
|
|
|
526
530
|
}
|
|
527
531
|
}
|
|
528
532
|
|
|
533
|
+
// src/utils/slug.ts
|
|
534
|
+
function slugify(text) {
|
|
535
|
+
return text.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 50) || "test";
|
|
536
|
+
}
|
|
537
|
+
function testFilename(testId, title, testType) {
|
|
538
|
+
const ext = testType === "k6" ? ".k6.ts" : ".pw.ts";
|
|
539
|
+
const slug = slugify(title);
|
|
540
|
+
return `${slug}.${testId}${ext}`;
|
|
541
|
+
}
|
|
542
|
+
var UUID_EXTRACT_REGEX = /(?:^|\.)([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i;
|
|
543
|
+
function extractUuidFromFilename(filename) {
|
|
544
|
+
const base = filename.split(/[/\\]/).pop() ?? filename;
|
|
545
|
+
const stem = base.replace(/\.(pw|k6)\.ts$/, "");
|
|
546
|
+
const match = UUID_EXTRACT_REGEX.exec(stem);
|
|
547
|
+
return match ? match[1] : void 0;
|
|
548
|
+
}
|
|
549
|
+
function stripTitleMetadata(script) {
|
|
550
|
+
let result = script.replace(/^\/\/\s*@title\s+[^\n]+\n\n?/, "");
|
|
551
|
+
result = result.replace(/\n\s*\*\s*@title\s+[^\n]+/, "");
|
|
552
|
+
return result;
|
|
553
|
+
}
|
|
554
|
+
|
|
529
555
|
// src/utils/resources.ts
|
|
530
|
-
var UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
531
556
|
function buildLocalResources(config, cwd) {
|
|
532
557
|
const resources = [];
|
|
533
558
|
if (config.jobs) {
|
|
@@ -536,7 +561,10 @@ function buildLocalResources(config, cwd) {
|
|
|
536
561
|
id: job.id,
|
|
537
562
|
type: "job",
|
|
538
563
|
name: job.name,
|
|
539
|
-
definition: {
|
|
564
|
+
definition: {
|
|
565
|
+
...job,
|
|
566
|
+
tests: normalizeJobTestsToIds(job.tests)
|
|
567
|
+
}
|
|
540
568
|
});
|
|
541
569
|
}
|
|
542
570
|
}
|
|
@@ -596,20 +624,24 @@ function buildLocalResources(config, cwd) {
|
|
|
596
624
|
for (const file of files) {
|
|
597
625
|
const script = readFileContent(file.absolutePath);
|
|
598
626
|
if (script) {
|
|
599
|
-
const
|
|
600
|
-
const testId = UUID_REGEX.test(stem) ? stem : void 0;
|
|
627
|
+
const testId = extractUuidFromFilename(file.filename);
|
|
601
628
|
if (!testId) {
|
|
602
629
|
logger.debug(`Test file "${file.filename}" does not have a UUID-based filename \u2014 will be treated as a new resource`);
|
|
603
630
|
}
|
|
631
|
+
const titleMatch = script.match(/@title\s+(.+)$/m);
|
|
632
|
+
const stem = file.filename.replace(/\.(pw|k6)\.ts$/, "");
|
|
633
|
+
const title = titleMatch ? titleMatch[1].trim() : stem;
|
|
634
|
+
const cleanScript = stripTitleMetadata(script);
|
|
635
|
+
const displayName = file.relativePath.replace(/^_supercheck_\//, "");
|
|
604
636
|
resources.push({
|
|
605
637
|
id: testId,
|
|
606
638
|
type: "test",
|
|
607
|
-
name:
|
|
639
|
+
name: displayName,
|
|
608
640
|
definition: {
|
|
609
641
|
id: testId,
|
|
610
|
-
title
|
|
642
|
+
title,
|
|
611
643
|
testType: file.type === "playwright" ? "playwright" : "k6",
|
|
612
|
-
script
|
|
644
|
+
script: cleanScript
|
|
613
645
|
}
|
|
614
646
|
});
|
|
615
647
|
}
|
|
@@ -699,13 +731,20 @@ function normalizeRemoteRaw(type, raw) {
|
|
|
699
731
|
if (!normalized.alertConfig) delete normalized.alertConfig;
|
|
700
732
|
}
|
|
701
733
|
}
|
|
734
|
+
if (type === "test") {
|
|
735
|
+
if (normalized.type === "browser") normalized.testType = "playwright";
|
|
736
|
+
else if (normalized.type === "performance") normalized.testType = "k6";
|
|
737
|
+
delete normalized.type;
|
|
738
|
+
if (typeof normalized.script === "string") {
|
|
739
|
+
normalized.script = stripTitleMetadata(normalized.script);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
702
742
|
if (type === "job") {
|
|
703
743
|
if (Array.isArray(normalized.tests)) {
|
|
704
744
|
normalized.tests = normalized.tests.map((t) => {
|
|
705
|
-
if (typeof t === "string") return t;
|
|
745
|
+
if (typeof t === "string") return extractUuidFromFilename(t) ?? t;
|
|
706
746
|
if (t && typeof t === "object" && t.id) {
|
|
707
|
-
|
|
708
|
-
return `_supercheck_/tests/${t.id}.${testType}.ts`;
|
|
747
|
+
return String(t.id);
|
|
709
748
|
}
|
|
710
749
|
return t;
|
|
711
750
|
});
|
|
@@ -730,6 +769,13 @@ function normalizeRemoteRaw(type, raw) {
|
|
|
730
769
|
}
|
|
731
770
|
return normalized;
|
|
732
771
|
}
|
|
772
|
+
function normalizeJobTestsToIds(tests) {
|
|
773
|
+
if (!tests) return tests;
|
|
774
|
+
return tests.map((t) => {
|
|
775
|
+
const id = extractUuidFromFilename(t);
|
|
776
|
+
return id ?? t;
|
|
777
|
+
});
|
|
778
|
+
}
|
|
733
779
|
async function fetchRemoteResources(client) {
|
|
734
780
|
const resources = [];
|
|
735
781
|
try {
|
|
@@ -746,7 +792,7 @@ async function fetchRemoteResources(client) {
|
|
|
746
792
|
logFetchError("jobs", err);
|
|
747
793
|
}
|
|
748
794
|
try {
|
|
749
|
-
const tests = await fetchAllPages(client, "/api/tests", "tests");
|
|
795
|
+
const tests = await fetchAllPages(client, "/api/tests?includeScript=true", "tests");
|
|
750
796
|
for (const test of tests) {
|
|
751
797
|
const id = safeId(test.id);
|
|
752
798
|
if (!id) {
|
|
@@ -1194,11 +1240,11 @@ var alertConfigSchema = z.object({
|
|
|
1194
1240
|
customMessage: z.string().optional()
|
|
1195
1241
|
}).passthrough();
|
|
1196
1242
|
var playwrightTestConfigSchema = z.object({
|
|
1197
|
-
testMatch: z.string().default("_supercheck_/
|
|
1243
|
+
testMatch: z.string().default("_supercheck_/playwright/**/*.pw.ts"),
|
|
1198
1244
|
browser: z.enum(["chromium", "firefox", "webkit"]).default("chromium")
|
|
1199
1245
|
});
|
|
1200
1246
|
var k6TestConfigSchema = z.object({
|
|
1201
|
-
testMatch: z.string().default("_supercheck_/
|
|
1247
|
+
testMatch: z.string().default("_supercheck_/k6/**/*.k6.ts")
|
|
1202
1248
|
});
|
|
1203
1249
|
var testsConfigSchema = z.object({
|
|
1204
1250
|
playwright: playwrightTestConfigSchema.optional(),
|
|
@@ -1372,6 +1418,8 @@ function validateNoSecrets(config) {
|
|
|
1372
1418
|
}
|
|
1373
1419
|
async function loadConfig(options = {}) {
|
|
1374
1420
|
const cwd = options.cwd ?? process.cwd();
|
|
1421
|
+
const { config: loadEnv } = await import("dotenv");
|
|
1422
|
+
loadEnv({ path: resolve2(cwd, ".env") });
|
|
1375
1423
|
const configPath = resolveConfigPath(cwd, options.configPath);
|
|
1376
1424
|
if (!configPath) {
|
|
1377
1425
|
throw new CLIError(
|
|
@@ -1481,21 +1529,21 @@ var locationsCommand = new Command2("locations").description("List available exe
|
|
|
1481
1529
|
// src/commands/config.ts
|
|
1482
1530
|
import { Command as Command3 } from "commander";
|
|
1483
1531
|
var configCommand = new Command3("config").description("Configuration management");
|
|
1484
|
-
configCommand.command("validate").description("Validate supercheck.config.ts").action(async () => {
|
|
1485
|
-
const { config, configPath } = await loadConfig();
|
|
1532
|
+
configCommand.command("validate").description("Validate supercheck.config.ts").option("--config <path>", "Path to config file").action(async (options) => {
|
|
1533
|
+
const { config, configPath } = await loadConfig({ configPath: options.config });
|
|
1486
1534
|
logger.success(`Valid configuration loaded from ${configPath}`);
|
|
1487
1535
|
logger.info(` Organization: ${config.project.organization}`);
|
|
1488
1536
|
logger.info(` Project: ${config.project.project}`);
|
|
1489
1537
|
});
|
|
1490
|
-
configCommand.command("print").description("Print the resolved configuration").action(async () => {
|
|
1491
|
-
const { config } = await loadConfig();
|
|
1538
|
+
configCommand.command("print").description("Print the resolved configuration").option("--config <path>", "Path to config file").action(async (options) => {
|
|
1539
|
+
const { config } = await loadConfig({ configPath: options.config });
|
|
1492
1540
|
output(config);
|
|
1493
1541
|
});
|
|
1494
1542
|
|
|
1495
1543
|
// src/commands/init.ts
|
|
1496
1544
|
import { Command as Command4 } from "commander";
|
|
1497
|
-
import { existsSync as
|
|
1498
|
-
import { resolve as
|
|
1545
|
+
import { existsSync as existsSync4, mkdirSync, writeFileSync as writeFileSync2, readFileSync as readFileSync2, appendFileSync } from "fs";
|
|
1546
|
+
import { resolve as resolve5 } from "path";
|
|
1499
1547
|
|
|
1500
1548
|
// src/utils/package-manager.ts
|
|
1501
1549
|
import { existsSync as existsSync2, writeFileSync } from "fs";
|
|
@@ -1549,9 +1597,9 @@ async function installDependencies(cwd, pm, opts = {}) {
|
|
|
1549
1597
|
await withSpinner(
|
|
1550
1598
|
`Installing ${devDeps.length} packages...`,
|
|
1551
1599
|
async () => {
|
|
1552
|
-
const { execSync } = await import("child_process");
|
|
1600
|
+
const { execSync: execSync2 } = await import("child_process");
|
|
1553
1601
|
try {
|
|
1554
|
-
|
|
1602
|
+
execSync2(cmd, {
|
|
1555
1603
|
cwd,
|
|
1556
1604
|
stdio: "pipe",
|
|
1557
1605
|
timeout: 12e4,
|
|
@@ -1573,6 +1621,205 @@ Error: ${message}`,
|
|
|
1573
1621
|
);
|
|
1574
1622
|
}
|
|
1575
1623
|
|
|
1624
|
+
// src/utils/deps.ts
|
|
1625
|
+
import { execSync } from "child_process";
|
|
1626
|
+
import { existsSync as existsSync3 } from "fs";
|
|
1627
|
+
import { resolve as resolve4 } from "path";
|
|
1628
|
+
import pc3 from "picocolors";
|
|
1629
|
+
function isCommandAvailable(command) {
|
|
1630
|
+
try {
|
|
1631
|
+
const output2 = execSync(`${command} --version`, {
|
|
1632
|
+
stdio: "pipe",
|
|
1633
|
+
timeout: 1e4,
|
|
1634
|
+
env: { ...process.env }
|
|
1635
|
+
}).toString().trim();
|
|
1636
|
+
const version = output2.split("\n")[0].trim();
|
|
1637
|
+
return { available: true, version };
|
|
1638
|
+
} catch {
|
|
1639
|
+
return { available: false };
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
function isPlaywrightPackageInstalled(cwd) {
|
|
1643
|
+
try {
|
|
1644
|
+
const pkgPath = resolve4(cwd, "node_modules", "@playwright", "test", "package.json");
|
|
1645
|
+
return existsSync3(pkgPath);
|
|
1646
|
+
} catch {
|
|
1647
|
+
return false;
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
function isK6Installed() {
|
|
1651
|
+
const result = isCommandAvailable("k6");
|
|
1652
|
+
return { installed: result.available, version: result.version };
|
|
1653
|
+
}
|
|
1654
|
+
function isNodeInstalled() {
|
|
1655
|
+
return { installed: true, version: process.version };
|
|
1656
|
+
}
|
|
1657
|
+
async function installPlaywrightBrowsers(cwd, browser = "chromium") {
|
|
1658
|
+
try {
|
|
1659
|
+
const allowedBrowsers = ["chromium", "firefox", "webkit", "chrome", "msedge"];
|
|
1660
|
+
if (!allowedBrowsers.includes(browser)) {
|
|
1661
|
+
logger.error(`Invalid browser: ${browser}. Allowed: ${allowedBrowsers.join(", ")}`);
|
|
1662
|
+
return false;
|
|
1663
|
+
}
|
|
1664
|
+
logger.info(`Installing Playwright ${browser} browser...`);
|
|
1665
|
+
execSync(`npx playwright install ${browser}`, {
|
|
1666
|
+
cwd,
|
|
1667
|
+
stdio: "inherit",
|
|
1668
|
+
timeout: 3e5,
|
|
1669
|
+
// 5 min timeout for browser download
|
|
1670
|
+
env: { ...process.env }
|
|
1671
|
+
});
|
|
1672
|
+
return true;
|
|
1673
|
+
} catch {
|
|
1674
|
+
return false;
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
function checkAllDependencies(cwd) {
|
|
1678
|
+
const deps = [];
|
|
1679
|
+
const node = isNodeInstalled();
|
|
1680
|
+
deps.push({
|
|
1681
|
+
name: "Node.js",
|
|
1682
|
+
installed: node.installed,
|
|
1683
|
+
version: node.version,
|
|
1684
|
+
required: true,
|
|
1685
|
+
installHint: "https://nodejs.org/"
|
|
1686
|
+
});
|
|
1687
|
+
const pwPkg = isPlaywrightPackageInstalled(cwd);
|
|
1688
|
+
deps.push({
|
|
1689
|
+
name: "@playwright/test",
|
|
1690
|
+
installed: pwPkg,
|
|
1691
|
+
detail: pwPkg ? "npm package found" : "npm package not found",
|
|
1692
|
+
required: true,
|
|
1693
|
+
installHint: "npm install --save-dev @playwright/test"
|
|
1694
|
+
});
|
|
1695
|
+
if (pwPkg) {
|
|
1696
|
+
const pwCheck = isCommandAvailable("npx playwright --version");
|
|
1697
|
+
deps.push({
|
|
1698
|
+
name: "Playwright browsers",
|
|
1699
|
+
installed: pwCheck.available,
|
|
1700
|
+
version: pwCheck.version,
|
|
1701
|
+
detail: pwCheck.available ? "browsers available" : "run: npx playwright install chromium",
|
|
1702
|
+
required: true,
|
|
1703
|
+
installHint: "npx playwright install chromium"
|
|
1704
|
+
});
|
|
1705
|
+
} else {
|
|
1706
|
+
deps.push({
|
|
1707
|
+
name: "Playwright browsers",
|
|
1708
|
+
installed: false,
|
|
1709
|
+
detail: "install @playwright/test first",
|
|
1710
|
+
required: true,
|
|
1711
|
+
installHint: "npm install --save-dev @playwright/test && npx playwright install chromium"
|
|
1712
|
+
});
|
|
1713
|
+
}
|
|
1714
|
+
const k6 = isK6Installed();
|
|
1715
|
+
deps.push({
|
|
1716
|
+
name: "k6",
|
|
1717
|
+
installed: k6.installed,
|
|
1718
|
+
version: k6.version,
|
|
1719
|
+
detail: k6.installed ? void 0 : "needed for performance tests",
|
|
1720
|
+
required: false,
|
|
1721
|
+
installHint: getK6InstallHint()
|
|
1722
|
+
});
|
|
1723
|
+
return deps;
|
|
1724
|
+
}
|
|
1725
|
+
function getK6InstallHint() {
|
|
1726
|
+
const platform = process.platform;
|
|
1727
|
+
switch (platform) {
|
|
1728
|
+
case "darwin":
|
|
1729
|
+
return "brew install k6";
|
|
1730
|
+
case "linux":
|
|
1731
|
+
return "sudo snap install k6 or see https://grafana.com/docs/k6/latest/set-up/install-k6/";
|
|
1732
|
+
case "win32":
|
|
1733
|
+
return "choco install k6 or winget install k6";
|
|
1734
|
+
default:
|
|
1735
|
+
return "https://grafana.com/docs/k6/latest/set-up/install-k6/";
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
function formatDependencyReport(deps) {
|
|
1739
|
+
const lines = [];
|
|
1740
|
+
let allGood = true;
|
|
1741
|
+
for (const dep of deps) {
|
|
1742
|
+
const icon = dep.installed ? pc3.green("\u2713") : dep.required ? pc3.red("\u2717") : pc3.yellow("\u25CB");
|
|
1743
|
+
const name = dep.installed ? dep.name : pc3.dim(dep.name);
|
|
1744
|
+
const version = dep.version ? pc3.dim(` (${dep.version})`) : "";
|
|
1745
|
+
const detail = dep.detail && !dep.installed ? pc3.dim(` \u2014 ${dep.detail}`) : "";
|
|
1746
|
+
lines.push(` ${icon} ${name}${version}${detail}`);
|
|
1747
|
+
if (!dep.installed && dep.required) {
|
|
1748
|
+
allGood = false;
|
|
1749
|
+
lines.push(` ${pc3.dim("Install:")} ${dep.installHint}`);
|
|
1750
|
+
} else if (!dep.installed && !dep.required) {
|
|
1751
|
+
lines.push(` ${pc3.dim("Install (optional):")} ${dep.installHint}`);
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
if (allGood) {
|
|
1755
|
+
lines.push("");
|
|
1756
|
+
lines.push(pc3.green(" All required dependencies are installed!"));
|
|
1757
|
+
}
|
|
1758
|
+
return lines.join("\n");
|
|
1759
|
+
}
|
|
1760
|
+
function ensureDependenciesForTestType(cwd, testType) {
|
|
1761
|
+
if (testType === "playwright") {
|
|
1762
|
+
if (!isPlaywrightPackageInstalled(cwd)) {
|
|
1763
|
+
throw new DependencyError(
|
|
1764
|
+
"playwright",
|
|
1765
|
+
"@playwright/test is not installed.\n Install it with: npm install --save-dev @playwright/test\n Then install browsers: npx playwright install chromium"
|
|
1766
|
+
);
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
if (testType === "k6") {
|
|
1770
|
+
const k6 = isK6Installed();
|
|
1771
|
+
if (!k6.installed) {
|
|
1772
|
+
throw new DependencyError(
|
|
1773
|
+
"k6",
|
|
1774
|
+
`k6 is not installed.
|
|
1775
|
+
Install it with: ${getK6InstallHint()}
|
|
1776
|
+
Documentation: https://grafana.com/docs/k6/latest/set-up/install-k6/`
|
|
1777
|
+
);
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
var DependencyError = class extends Error {
|
|
1782
|
+
dependency;
|
|
1783
|
+
constructor(dependency, message) {
|
|
1784
|
+
super(message);
|
|
1785
|
+
this.name = "DependencyError";
|
|
1786
|
+
this.dependency = dependency;
|
|
1787
|
+
}
|
|
1788
|
+
};
|
|
1789
|
+
|
|
1790
|
+
// src/utils/uuid.ts
|
|
1791
|
+
import { randomBytes } from "crypto";
|
|
1792
|
+
function uuidv7() {
|
|
1793
|
+
const now = Date.now();
|
|
1794
|
+
const timestampBytes = new Uint8Array(6);
|
|
1795
|
+
let ts = now;
|
|
1796
|
+
for (let i = 5; i >= 0; i--) {
|
|
1797
|
+
timestampBytes[i] = ts & 255;
|
|
1798
|
+
ts = Math.floor(ts / 256);
|
|
1799
|
+
}
|
|
1800
|
+
const randBytes = randomBytes(10);
|
|
1801
|
+
const bytes = new Uint8Array(16);
|
|
1802
|
+
bytes.set(timestampBytes, 0);
|
|
1803
|
+
bytes[6] = 112 | randBytes[0] & 15;
|
|
1804
|
+
bytes[7] = randBytes[1];
|
|
1805
|
+
bytes[8] = 128 | randBytes[2] & 63;
|
|
1806
|
+
bytes[9] = randBytes[3];
|
|
1807
|
+
bytes[10] = randBytes[4];
|
|
1808
|
+
bytes[11] = randBytes[5];
|
|
1809
|
+
bytes[12] = randBytes[6];
|
|
1810
|
+
bytes[13] = randBytes[7];
|
|
1811
|
+
bytes[14] = randBytes[8];
|
|
1812
|
+
bytes[15] = randBytes[9];
|
|
1813
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
1814
|
+
return [
|
|
1815
|
+
hex.slice(0, 8),
|
|
1816
|
+
hex.slice(8, 12),
|
|
1817
|
+
hex.slice(12, 16),
|
|
1818
|
+
hex.slice(16, 20),
|
|
1819
|
+
hex.slice(20, 32)
|
|
1820
|
+
].join("-");
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1576
1823
|
// src/commands/init.ts
|
|
1577
1824
|
var CONFIG_TEMPLATE = `import { defineConfig } from '@supercheck/cli'
|
|
1578
1825
|
|
|
@@ -1587,23 +1834,27 @@ export default defineConfig({
|
|
|
1587
1834
|
},
|
|
1588
1835
|
tests: {
|
|
1589
1836
|
playwright: {
|
|
1590
|
-
testMatch: '_supercheck_/
|
|
1837
|
+
testMatch: '_supercheck_/playwright/**/*.pw.ts',
|
|
1591
1838
|
browser: 'chromium',
|
|
1592
1839
|
},
|
|
1593
1840
|
k6: {
|
|
1594
|
-
testMatch: '_supercheck_/
|
|
1841
|
+
testMatch: '_supercheck_/k6/**/*.k6.ts',
|
|
1595
1842
|
},
|
|
1596
1843
|
},
|
|
1597
1844
|
})
|
|
1598
1845
|
`;
|
|
1599
|
-
var EXAMPLE_PW_TEST =
|
|
1846
|
+
var EXAMPLE_PW_TEST = `// @title Homepage Check
|
|
1847
|
+
|
|
1848
|
+
import { test, expect } from '@playwright/test'
|
|
1600
1849
|
|
|
1601
1850
|
test('homepage loads successfully', async ({ page }) => {
|
|
1602
1851
|
await page.goto('https://example.com')
|
|
1603
1852
|
await expect(page).toHaveTitle(/Example/)
|
|
1604
1853
|
})
|
|
1605
1854
|
`;
|
|
1606
|
-
var EXAMPLE_K6_TEST =
|
|
1855
|
+
var EXAMPLE_K6_TEST = `// @title Load Test
|
|
1856
|
+
|
|
1857
|
+
import http from 'k6/http'
|
|
1607
1858
|
import { check, sleep } from 'k6'
|
|
1608
1859
|
|
|
1609
1860
|
export const options = {
|
|
@@ -1631,7 +1882,7 @@ var SUPERCHECK_TSCONFIG = `{
|
|
|
1631
1882
|
"resolveJsonModule": true,
|
|
1632
1883
|
"isolatedModules": true,
|
|
1633
1884
|
"noEmit": true,
|
|
1634
|
-
"types": ["node"]
|
|
1885
|
+
"types": ["node", "k6"]
|
|
1635
1886
|
},
|
|
1636
1887
|
"include": [
|
|
1637
1888
|
"supercheck.config.ts",
|
|
@@ -1648,8 +1899,8 @@ supercheck.config.local.mjs
|
|
|
1648
1899
|
`;
|
|
1649
1900
|
var initCommand = new Command4("init").description("Initialize a new Supercheck project with config and example tests").option("--force", "Overwrite existing config file").option("--skip-install", "Skip automatic dependency installation").option("--skip-examples", "Skip creating example test files").option("--pm <manager>", "Package manager to use (npm, yarn, pnpm, bun)").action(async (options) => {
|
|
1650
1901
|
const cwd = process.cwd();
|
|
1651
|
-
const configPath =
|
|
1652
|
-
if (
|
|
1902
|
+
const configPath = resolve5(cwd, "supercheck.config.ts");
|
|
1903
|
+
if (existsSync4(configPath) && !options.force) {
|
|
1653
1904
|
throw new CLIError(
|
|
1654
1905
|
"supercheck.config.ts already exists. Use --force to overwrite.",
|
|
1655
1906
|
3 /* ConfigError */
|
|
@@ -1660,43 +1911,42 @@ var initCommand = new Command4("init").description("Initialize a new Supercheck
|
|
|
1660
1911
|
logger.newline();
|
|
1661
1912
|
writeFileSync2(configPath, CONFIG_TEMPLATE, "utf-8");
|
|
1662
1913
|
logger.success("Created supercheck.config.ts");
|
|
1663
|
-
const tsconfigPath =
|
|
1664
|
-
if (!
|
|
1914
|
+
const tsconfigPath = resolve5(cwd, "tsconfig.supercheck.json");
|
|
1915
|
+
if (!existsSync4(tsconfigPath) || options.force) {
|
|
1665
1916
|
writeFileSync2(tsconfigPath, SUPERCHECK_TSCONFIG, "utf-8");
|
|
1666
1917
|
logger.success("Created tsconfig.supercheck.json (IDE IntelliSense)");
|
|
1667
1918
|
}
|
|
1668
|
-
const
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
const dirPath = resolve4(cwd, dir);
|
|
1675
|
-
if (!existsSync3(dirPath)) {
|
|
1676
|
-
mkdirSync(dirPath, { recursive: true });
|
|
1677
|
-
}
|
|
1678
|
-
if (dir !== "_supercheck_/tests") {
|
|
1679
|
-
const gitkeepPath = resolve4(dirPath, ".gitkeep");
|
|
1680
|
-
if (!existsSync3(gitkeepPath)) {
|
|
1681
|
-
writeFileSync2(gitkeepPath, "", "utf-8");
|
|
1682
|
-
}
|
|
1919
|
+
const supercheckDir = resolve5(cwd, "_supercheck_");
|
|
1920
|
+
const playwrightDir = resolve5(supercheckDir, "playwright");
|
|
1921
|
+
const k6Dir = resolve5(supercheckDir, "k6");
|
|
1922
|
+
for (const dir of [supercheckDir, playwrightDir, k6Dir]) {
|
|
1923
|
+
if (!existsSync4(dir)) {
|
|
1924
|
+
mkdirSync(dir, { recursive: true });
|
|
1683
1925
|
}
|
|
1684
1926
|
}
|
|
1685
1927
|
logger.success("Created _supercheck_/ directory structure");
|
|
1686
1928
|
if (!options.skipExamples) {
|
|
1687
|
-
const
|
|
1688
|
-
|
|
1929
|
+
const pwTitle = "Homepage Check";
|
|
1930
|
+
const pwId = uuidv7();
|
|
1931
|
+
const pwSlug = slugify(pwTitle);
|
|
1932
|
+
const pwFilename = `${pwSlug}.${pwId}.pw.ts`;
|
|
1933
|
+
const pwTestPath = resolve5(cwd, `_supercheck_/playwright/${pwFilename}`);
|
|
1934
|
+
if (!existsSync4(pwTestPath)) {
|
|
1689
1935
|
writeFileSync2(pwTestPath, EXAMPLE_PW_TEST, "utf-8");
|
|
1690
|
-
logger.success(
|
|
1691
|
-
}
|
|
1692
|
-
const
|
|
1693
|
-
|
|
1936
|
+
logger.success(`Created _supercheck_/playwright/${pwFilename} (Playwright example)`);
|
|
1937
|
+
}
|
|
1938
|
+
const k6Title = "Load Test";
|
|
1939
|
+
const k6Id = uuidv7();
|
|
1940
|
+
const k6Slug = slugify(k6Title);
|
|
1941
|
+
const k6Filename = `${k6Slug}.${k6Id}.k6.ts`;
|
|
1942
|
+
const k6TestPath = resolve5(cwd, `_supercheck_/k6/${k6Filename}`);
|
|
1943
|
+
if (!existsSync4(k6TestPath)) {
|
|
1694
1944
|
writeFileSync2(k6TestPath, EXAMPLE_K6_TEST, "utf-8");
|
|
1695
|
-
logger.success(
|
|
1945
|
+
logger.success(`Created _supercheck_/k6/${k6Filename} (k6 example)`);
|
|
1696
1946
|
}
|
|
1697
1947
|
}
|
|
1698
|
-
const gitignorePath =
|
|
1699
|
-
if (
|
|
1948
|
+
const gitignorePath = resolve5(cwd, ".gitignore");
|
|
1949
|
+
if (existsSync4(gitignorePath)) {
|
|
1700
1950
|
const content = readFileSync2(gitignorePath, "utf-8");
|
|
1701
1951
|
if (!content.includes("supercheck.config.local")) {
|
|
1702
1952
|
appendFileSync(gitignorePath, GITIGNORE_ADDITIONS, "utf-8");
|
|
@@ -1710,16 +1960,33 @@ var initCommand = new Command4("init").description("Initialize a new Supercheck
|
|
|
1710
1960
|
logger.success("Created package.json");
|
|
1711
1961
|
}
|
|
1712
1962
|
await installDependencies(cwd, pm, {
|
|
1713
|
-
packages: options.skipExamples ? ["@supercheck/cli", "typescript", "@types/node"] : ["@supercheck/cli", "typescript", "@types/node", "@playwright/test"],
|
|
1963
|
+
packages: options.skipExamples ? ["@supercheck/cli", "typescript", "@types/node", "@types/k6"] : ["@supercheck/cli", "typescript", "@types/node", "@playwright/test", "@types/k6"],
|
|
1714
1964
|
skipInstall: options.skipInstall ?? false
|
|
1715
1965
|
});
|
|
1966
|
+
if (!options.skipInstall && !options.skipExamples) {
|
|
1967
|
+
logger.newline();
|
|
1968
|
+
const browserInstalled = await installPlaywrightBrowsers(cwd, "chromium");
|
|
1969
|
+
if (browserInstalled) {
|
|
1970
|
+
logger.success("Playwright chromium browser installed");
|
|
1971
|
+
} else {
|
|
1972
|
+
logger.warn("Could not install Playwright browsers automatically.");
|
|
1973
|
+
logger.info(" Run manually: npx playwright install chromium");
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
const k6Status = isK6Installed();
|
|
1716
1977
|
logger.newline();
|
|
1717
1978
|
logger.header("Supercheck project initialized!");
|
|
1718
1979
|
logger.newline();
|
|
1980
|
+
if (!k6Status.installed) {
|
|
1981
|
+
logger.warn("k6 is not installed (needed for performance/load tests)");
|
|
1982
|
+
logger.info(` Install: ${getK6InstallHint()}`);
|
|
1983
|
+
logger.info(" Docs: https://grafana.com/docs/k6/latest/set-up/install-k6/");
|
|
1984
|
+
logger.newline();
|
|
1985
|
+
}
|
|
1719
1986
|
logger.info("Next steps:");
|
|
1720
1987
|
logger.info(" 1. Edit supercheck.config.ts with your org/project details");
|
|
1721
1988
|
logger.info(" 2. Run `supercheck login --token <your-token>` to authenticate");
|
|
1722
|
-
logger.info(" 3. Write tests in _supercheck_/
|
|
1989
|
+
logger.info(" 3. Write tests in _supercheck_/playwright and _supercheck_/k6");
|
|
1723
1990
|
logger.info(" 4. Run `supercheck diff` to preview changes against the cloud");
|
|
1724
1991
|
logger.info(" 5. Run `supercheck deploy` to push to Supercheck");
|
|
1725
1992
|
logger.info(" 6. Run `supercheck pull` to sync cloud resources locally");
|
|
@@ -1734,6 +2001,8 @@ var initCommand = new Command4("init").description("Initialize a new Supercheck
|
|
|
1734
2001
|
|
|
1735
2002
|
// src/commands/jobs.ts
|
|
1736
2003
|
import { Command as Command5 } from "commander";
|
|
2004
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
2005
|
+
import { resolve as resolve6 } from "path";
|
|
1737
2006
|
|
|
1738
2007
|
// src/api/authenticated-client.ts
|
|
1739
2008
|
function createAuthenticatedClient() {
|
|
@@ -1742,7 +2011,7 @@ function createAuthenticatedClient() {
|
|
|
1742
2011
|
return getApiClient({ token, baseUrl: baseUrl ?? void 0 });
|
|
1743
2012
|
}
|
|
1744
2013
|
|
|
1745
|
-
// src/utils/
|
|
2014
|
+
// src/utils/number.ts
|
|
1746
2015
|
function parseIntStrict(value, name, opts) {
|
|
1747
2016
|
const parsed = parseInt(value, 10);
|
|
1748
2017
|
if (!Number.isFinite(parsed)) {
|
|
@@ -1766,8 +2035,190 @@ function parseIntStrict(value, name, opts) {
|
|
|
1766
2035
|
return parsed;
|
|
1767
2036
|
}
|
|
1768
2037
|
|
|
2038
|
+
// src/utils/validation.ts
|
|
2039
|
+
function normalizeTestTypeForApi(localType) {
|
|
2040
|
+
if (typeof localType !== "string") return void 0;
|
|
2041
|
+
const normalized = localType.trim().toLowerCase();
|
|
2042
|
+
if (normalized === "playwright") return "browser";
|
|
2043
|
+
if (normalized === "k6") return "performance";
|
|
2044
|
+
if (normalized === "load") return "performance";
|
|
2045
|
+
if (normalized === "browser" || normalized === "performance" || normalized === "api" || normalized === "database" || normalized === "custom") {
|
|
2046
|
+
return normalized;
|
|
2047
|
+
}
|
|
2048
|
+
return void 0;
|
|
2049
|
+
}
|
|
2050
|
+
function formatValidationError(responseBody) {
|
|
2051
|
+
if (responseBody && typeof responseBody === "object") {
|
|
2052
|
+
const body = responseBody;
|
|
2053
|
+
if (typeof body.error === "string") {
|
|
2054
|
+
const line = typeof body.line === "number" ? body.line : void 0;
|
|
2055
|
+
const column = typeof body.column === "number" ? body.column : void 0;
|
|
2056
|
+
if (line !== void 0 && column !== void 0) {
|
|
2057
|
+
return `${body.error} (line ${line}, column ${column})`;
|
|
2058
|
+
}
|
|
2059
|
+
return body.error;
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
return "Validation failed";
|
|
2063
|
+
}
|
|
2064
|
+
async function validateScripts(client, inputs) {
|
|
2065
|
+
const results = [];
|
|
2066
|
+
for (const input of inputs) {
|
|
2067
|
+
if (!input.script || input.script.trim().length === 0) {
|
|
2068
|
+
throw new CLIError(`Test "${input.name}" has no script content`, 3 /* ConfigError */);
|
|
2069
|
+
}
|
|
2070
|
+
try {
|
|
2071
|
+
const { data } = await client.post(
|
|
2072
|
+
"/api/validate-script",
|
|
2073
|
+
{
|
|
2074
|
+
script: input.script,
|
|
2075
|
+
testType: input.testType
|
|
2076
|
+
}
|
|
2077
|
+
);
|
|
2078
|
+
results.push({
|
|
2079
|
+
name: input.name,
|
|
2080
|
+
valid: data?.valid ?? true,
|
|
2081
|
+
warnings: data?.warnings
|
|
2082
|
+
});
|
|
2083
|
+
} catch (err) {
|
|
2084
|
+
if (err instanceof ApiRequestError) {
|
|
2085
|
+
results.push({
|
|
2086
|
+
name: input.name,
|
|
2087
|
+
valid: false,
|
|
2088
|
+
error: formatValidationError(err.responseBody)
|
|
2089
|
+
});
|
|
2090
|
+
continue;
|
|
2091
|
+
}
|
|
2092
|
+
throw err;
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
return results;
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
// src/utils/exec.ts
|
|
2099
|
+
import { spawn } from "child_process";
|
|
2100
|
+
function runCommand(command, args, cwd) {
|
|
2101
|
+
return new Promise((resolve10, reject) => {
|
|
2102
|
+
const executable = process.platform === "win32" && command === "npx" ? "npx.cmd" : command;
|
|
2103
|
+
const child = spawn(executable, args, {
|
|
2104
|
+
stdio: "inherit",
|
|
2105
|
+
shell: false,
|
|
2106
|
+
// ✓ SECURE: Prevents command injection
|
|
2107
|
+
cwd
|
|
2108
|
+
});
|
|
2109
|
+
child.on("error", (err) => reject(err));
|
|
2110
|
+
child.on("close", (code) => resolve10(code ?? 0));
|
|
2111
|
+
});
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
// src/utils/playwright.ts
|
|
2115
|
+
import { mkdtempSync, writeFileSync as writeFileSync3, rmSync } from "fs";
|
|
2116
|
+
import { join } from "path";
|
|
2117
|
+
function createTempPlaywrightConfig(cwd, testMatch) {
|
|
2118
|
+
const dir = mkdtempSync(join(cwd, ".supercheck-playwright-"));
|
|
2119
|
+
const filePath = join(dir, "playwright.supercheck.config.mjs");
|
|
2120
|
+
const match = testMatch ?? "_supercheck_/playwright/**/*.pw.ts";
|
|
2121
|
+
const contents = [
|
|
2122
|
+
"import { defineConfig } from '@playwright/test'",
|
|
2123
|
+
"",
|
|
2124
|
+
"export default defineConfig({",
|
|
2125
|
+
` testDir: ${JSON.stringify(cwd)},`,
|
|
2126
|
+
` testMatch: ${JSON.stringify([match])},`,
|
|
2127
|
+
"})",
|
|
2128
|
+
""
|
|
2129
|
+
].join("\n");
|
|
2130
|
+
writeFileSync3(filePath, contents, "utf-8");
|
|
2131
|
+
return {
|
|
2132
|
+
path: filePath,
|
|
2133
|
+
cleanup: () => {
|
|
2134
|
+
rmSync(dir, { recursive: true, force: true });
|
|
2135
|
+
}
|
|
2136
|
+
};
|
|
2137
|
+
}
|
|
2138
|
+
|
|
1769
2139
|
// src/commands/jobs.ts
|
|
1770
2140
|
var jobCommand = new Command5("job").description("Manage jobs");
|
|
2141
|
+
function inferLocalTestType(filePath) {
|
|
2142
|
+
return filePath.endsWith(".k6.ts") || filePath.endsWith(".k6.js") ? "k6" : "playwright";
|
|
2143
|
+
}
|
|
2144
|
+
function toDisplayName(cwd, filePath) {
|
|
2145
|
+
const normalized = filePath.replace(cwd, "").replace(/^\//, "");
|
|
2146
|
+
return normalized.startsWith("_supercheck_/") ? normalized.replace(/^_supercheck_\//, "") : normalized;
|
|
2147
|
+
}
|
|
2148
|
+
function resolveJobLocalTests(cwd, tests, patterns) {
|
|
2149
|
+
const files = discoverFiles(cwd, patterns);
|
|
2150
|
+
const uuidMap = /* @__PURE__ */ new Map();
|
|
2151
|
+
for (const file of files) {
|
|
2152
|
+
const uuid = extractUuidFromFilename(file.absolutePath);
|
|
2153
|
+
if (uuid) uuidMap.set(uuid, file.absolutePath);
|
|
2154
|
+
}
|
|
2155
|
+
return tests.map((ref) => {
|
|
2156
|
+
let resolved;
|
|
2157
|
+
if (ref.endsWith(".pw.ts") || ref.endsWith(".k6.ts") || ref.endsWith(".pw.js") || ref.endsWith(".k6.js")) {
|
|
2158
|
+
resolved = resolve6(cwd, ref);
|
|
2159
|
+
} else if (ref.startsWith("_supercheck_/")) {
|
|
2160
|
+
resolved = resolve6(cwd, ref);
|
|
2161
|
+
} else if (uuidMap.has(ref)) {
|
|
2162
|
+
resolved = uuidMap.get(ref);
|
|
2163
|
+
}
|
|
2164
|
+
if (!resolved) {
|
|
2165
|
+
throw new CLIError(`Cannot resolve local test for job entry: ${ref}`, 3 /* ConfigError */);
|
|
2166
|
+
}
|
|
2167
|
+
const script = readFileSync3(resolved, "utf-8");
|
|
2168
|
+
const type = inferLocalTestType(resolved);
|
|
2169
|
+
return { path: resolved, type, name: toDisplayName(cwd, resolved), script };
|
|
2170
|
+
});
|
|
2171
|
+
}
|
|
2172
|
+
async function validateJobTests(client, tests) {
|
|
2173
|
+
const inputs = tests.map((test) => ({
|
|
2174
|
+
name: test.name,
|
|
2175
|
+
script: test.script,
|
|
2176
|
+
testType: normalizeTestTypeForApi(test.type)
|
|
2177
|
+
}));
|
|
2178
|
+
const results = await validateScripts(client, inputs);
|
|
2179
|
+
const failures = results.filter((r) => !r.valid);
|
|
2180
|
+
if (failures.length > 0) {
|
|
2181
|
+
const details = failures.map((f) => ` - ${f.name}: ${f.error ?? "Validation failed"}`).join("\n");
|
|
2182
|
+
throw new CLIError(`Validation failed:
|
|
2183
|
+
${details}`, 3 /* ConfigError */);
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
async function runLocalJobTests(tests, cwd, testMatch) {
|
|
2187
|
+
const playwrightTests = tests.filter((t) => t.type === "playwright").map((t) => t.path);
|
|
2188
|
+
const k6Tests = tests.filter((t) => t.type === "k6").map((t) => t.path);
|
|
2189
|
+
try {
|
|
2190
|
+
if (playwrightTests.length > 0) {
|
|
2191
|
+
ensureDependenciesForTestType(cwd, "playwright");
|
|
2192
|
+
}
|
|
2193
|
+
if (k6Tests.length > 0) {
|
|
2194
|
+
ensureDependenciesForTestType(cwd, "k6");
|
|
2195
|
+
}
|
|
2196
|
+
} catch (err) {
|
|
2197
|
+
if (err instanceof DependencyError) {
|
|
2198
|
+
throw new CLIError(err.message, 1 /* GeneralError */);
|
|
2199
|
+
}
|
|
2200
|
+
throw err;
|
|
2201
|
+
}
|
|
2202
|
+
if (playwrightTests.length > 0) {
|
|
2203
|
+
logger.header("Running Playwright tests");
|
|
2204
|
+
const tempConfig = createTempPlaywrightConfig(cwd, testMatch);
|
|
2205
|
+
try {
|
|
2206
|
+
const code = await runCommand("npx", ["playwright", "test", "--config", tempConfig.path, ...playwrightTests], cwd);
|
|
2207
|
+
if (code !== 0) {
|
|
2208
|
+
throw new CLIError(`Playwright tests failed with exit code ${code}`, 1 /* GeneralError */);
|
|
2209
|
+
}
|
|
2210
|
+
} finally {
|
|
2211
|
+
tempConfig.cleanup();
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
for (const path of k6Tests) {
|
|
2215
|
+
logger.header(`Running k6 test: ${toDisplayName(cwd, path)}`);
|
|
2216
|
+
const code = await runCommand("k6", ["run", path], cwd);
|
|
2217
|
+
if (code !== 0) {
|
|
2218
|
+
throw new CLIError(`k6 test failed with exit code ${code}`, 1 /* GeneralError */);
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
1771
2222
|
jobCommand.command("list").description("List all jobs").option("--page <page>", "Page number", "1").option("--limit <limit>", "Items per page", "50").action(async (options) => {
|
|
1772
2223
|
const client = createAuthenticatedClient();
|
|
1773
2224
|
const { data } = await withSpinner(
|
|
@@ -1846,15 +2297,19 @@ jobCommand.command("get <id>").description("Get job details").action(async (id)
|
|
|
1846
2297
|
);
|
|
1847
2298
|
outputDetail(data);
|
|
1848
2299
|
});
|
|
1849
|
-
jobCommand.command("create").description("Create a new job").requiredOption("--name <name>", "Job name").option("--description <description>", "Job description", "").option("--type <type>", "Job type (playwright, k6)").option("--schedule <cron>", "Cron schedule expression").option("--timeout <seconds>", "Timeout in seconds", "300").option("--retries <count>", "Retry count on failure", "0").action(async (options) => {
|
|
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("--timeout <seconds>", "Timeout in seconds", "300").option("--retries <count>", "Retry count on failure", "0").action(async (options) => {
|
|
1850
2301
|
const client = createAuthenticatedClient();
|
|
2302
|
+
const tests = (options.tests ?? []).flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean);
|
|
2303
|
+
if (tests.length === 0) {
|
|
2304
|
+
throw new CLIError("At least one test ID is required to create a job.", 3 /* ConfigError */);
|
|
2305
|
+
}
|
|
1851
2306
|
const body = {
|
|
1852
2307
|
name: options.name,
|
|
1853
2308
|
description: options.description,
|
|
1854
2309
|
timeoutSeconds: parseIntStrict(options.timeout, "--timeout", { min: 1 }),
|
|
1855
2310
|
retryCount: parseIntStrict(options.retries, "--retries", { min: 0 }),
|
|
1856
2311
|
config: {},
|
|
1857
|
-
tests:
|
|
2312
|
+
tests: tests.map((id) => ({ id }))
|
|
1858
2313
|
};
|
|
1859
2314
|
if (options.type) body.jobType = options.type;
|
|
1860
2315
|
if (options.schedule) body.cronSchedule = options.schedule;
|
|
@@ -1862,8 +2317,10 @@ jobCommand.command("create").description("Create a new job").requiredOption("--n
|
|
|
1862
2317
|
"Creating job",
|
|
1863
2318
|
() => client.post("/api/jobs", body)
|
|
1864
2319
|
);
|
|
1865
|
-
|
|
1866
|
-
|
|
2320
|
+
const job = data.job ?? data;
|
|
2321
|
+
const jobId = job?.id;
|
|
2322
|
+
logger.success(`Job "${options.name}" created (${jobId ?? "unknown"})`);
|
|
2323
|
+
outputDetail(job);
|
|
1867
2324
|
});
|
|
1868
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("--timeout <seconds>", "Timeout in seconds").option("--retries <count>", "Retry count on failure").option("--status <status>", "Job status (active, paused)").action(async (id, options) => {
|
|
1869
2326
|
const client = createAuthenticatedClient();
|
|
@@ -1901,7 +2358,34 @@ jobCommand.command("delete <id>").description("Delete a job").option("--force",
|
|
|
1901
2358
|
);
|
|
1902
2359
|
logger.success(`Job ${id} deleted`);
|
|
1903
2360
|
});
|
|
1904
|
-
jobCommand.command("run").description("Run a job immediately").requiredOption("--id <id>", "Job ID to run").action(async (options) => {
|
|
2361
|
+
jobCommand.command("run").description("Run a job immediately").requiredOption("--id <id>", "Job ID to run").option("--local", "Run locally").option("--cloud", "Run on cloud").action(async (options) => {
|
|
2362
|
+
if (options.local && options.cloud) {
|
|
2363
|
+
throw new CLIError("--local and --cloud are mutually exclusive.", 3 /* ConfigError */);
|
|
2364
|
+
}
|
|
2365
|
+
const useLocal = options.local === true;
|
|
2366
|
+
if (useLocal) {
|
|
2367
|
+
const cwd = process.cwd();
|
|
2368
|
+
const { config } = await loadConfig({ cwd });
|
|
2369
|
+
const job = config.jobs?.find((j) => j.id === options.id || j.name === options.id);
|
|
2370
|
+
if (!job) {
|
|
2371
|
+
throw new CLIError(`Job ${options.id} not found in local config.`, 3 /* ConfigError */);
|
|
2372
|
+
}
|
|
2373
|
+
const tests2 = Array.isArray(job.tests) ? job.tests : [];
|
|
2374
|
+
if (tests2.length === 0) {
|
|
2375
|
+
throw new CLIError(`Job ${options.id} has no tests.`, 1 /* GeneralError */);
|
|
2376
|
+
}
|
|
2377
|
+
const patterns = {
|
|
2378
|
+
playwright: config.tests?.playwright?.testMatch,
|
|
2379
|
+
k6: config.tests?.k6?.testMatch
|
|
2380
|
+
};
|
|
2381
|
+
const localTests = resolveJobLocalTests(cwd, tests2, patterns);
|
|
2382
|
+
const client2 = createAuthenticatedClient();
|
|
2383
|
+
await withSpinner("Validating test scripts...", async () => {
|
|
2384
|
+
await validateJobTests(client2, localTests);
|
|
2385
|
+
}, { successText: "Test scripts validated" });
|
|
2386
|
+
await runLocalJobTests(localTests, cwd, config.tests?.playwright?.testMatch);
|
|
2387
|
+
return;
|
|
2388
|
+
}
|
|
1905
2389
|
const client = createAuthenticatedClient();
|
|
1906
2390
|
const { data: jobData } = await withSpinner(
|
|
1907
2391
|
"Fetching job details",
|
|
@@ -1924,7 +2408,7 @@ jobCommand.command("run").description("Run a job immediately").requiredOption("-
|
|
|
1924
2408
|
"Running job",
|
|
1925
2409
|
() => client.post(
|
|
1926
2410
|
"/api/jobs/run",
|
|
1927
|
-
{ jobId: options.id, tests: payloadTests, trigger: "
|
|
2411
|
+
{ jobId: options.id, tests: payloadTests, trigger: "remote" }
|
|
1928
2412
|
)
|
|
1929
2413
|
);
|
|
1930
2414
|
logger.success(`Job started. Run ID: ${data.runId}`);
|
|
@@ -1941,7 +2425,11 @@ jobCommand.command("trigger <id>").description("Trigger a job run").option("--wa
|
|
|
1941
2425
|
`/api/jobs/${id}/trigger`
|
|
1942
2426
|
)
|
|
1943
2427
|
);
|
|
1944
|
-
|
|
2428
|
+
const runId = data.runId ?? data.data?.runId;
|
|
2429
|
+
if (!runId) {
|
|
2430
|
+
throw new CLIError("Job trigger response did not include a run ID.", 4 /* ApiError */);
|
|
2431
|
+
}
|
|
2432
|
+
logger.success(`Job triggered. Run ID: ${runId}`);
|
|
1945
2433
|
if (options.wait) {
|
|
1946
2434
|
let statusClient;
|
|
1947
2435
|
try {
|
|
@@ -1956,20 +2444,20 @@ jobCommand.command("trigger <id>").description("Trigger a job run").option("--wa
|
|
|
1956
2444
|
const timeoutMs = parseIntStrict(options.timeout, "--timeout", { min: 1 }) * 1e3;
|
|
1957
2445
|
const startTime = Date.now();
|
|
1958
2446
|
while (Date.now() - startTime < timeoutMs) {
|
|
1959
|
-
await new Promise((
|
|
2447
|
+
await new Promise((resolve10) => setTimeout(resolve10, 3e3));
|
|
1960
2448
|
const { data: runData } = await statusClient.get(
|
|
1961
|
-
`/api/runs/${
|
|
2449
|
+
`/api/runs/${runId}`
|
|
1962
2450
|
);
|
|
1963
2451
|
const status = typeof runData.status === "string" ? runData.status.toLowerCase() : "";
|
|
1964
2452
|
if (["passed", "failed", "error", "blocked"].includes(status)) {
|
|
1965
2453
|
if (status === "passed") {
|
|
1966
|
-
logger.success(`Run ${
|
|
2454
|
+
logger.success(`Run ${runId} passed`);
|
|
1967
2455
|
} else if (status === "blocked") {
|
|
1968
|
-
logger.error(`Run ${
|
|
1969
|
-
throw new CLIError(`Run ${
|
|
2456
|
+
logger.error(`Run ${runId} blocked`);
|
|
2457
|
+
throw new CLIError(`Run ${runId} blocked`, 1 /* GeneralError */);
|
|
1970
2458
|
} else {
|
|
1971
|
-
logger.error(`Run ${
|
|
1972
|
-
throw new CLIError(`Run ${
|
|
2459
|
+
logger.error(`Run ${runId} ${status}`);
|
|
2460
|
+
throw new CLIError(`Run ${runId} ${status}`, 1 /* GeneralError */);
|
|
1973
2461
|
}
|
|
1974
2462
|
outputDetail(runData);
|
|
1975
2463
|
return;
|
|
@@ -1985,55 +2473,14 @@ jobCommand.command("trigger <id>").description("Trigger a job run").option("--wa
|
|
|
1985
2473
|
|
|
1986
2474
|
// src/commands/runs.ts
|
|
1987
2475
|
import { Command as Command6 } from "commander";
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
function getProxyAgent2(proxyUrl) {
|
|
1991
|
-
const existing = proxyAgents2.get(proxyUrl);
|
|
1992
|
-
if (existing) return existing;
|
|
1993
|
-
const created = new ProxyAgent2(proxyUrl);
|
|
1994
|
-
proxyAgents2.set(proxyUrl, created);
|
|
1995
|
-
return created;
|
|
1996
|
-
}
|
|
1997
|
-
function isNoProxyMatch(url, noProxyRaw) {
|
|
1998
|
-
const hostname = url.hostname;
|
|
1999
|
-
const port = url.port || (url.protocol === "https:" ? "443" : "80");
|
|
2000
|
-
const entries = noProxyRaw.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
2001
|
-
if (entries.includes("*")) return true;
|
|
2002
|
-
for (const entry of entries) {
|
|
2003
|
-
const [hostPart, portPart] = entry.split(":");
|
|
2004
|
-
const host = hostPart.trim();
|
|
2005
|
-
const entryPort = portPart?.trim();
|
|
2006
|
-
if (!host) continue;
|
|
2007
|
-
if (entryPort && entryPort !== port) continue;
|
|
2008
|
-
if (host.startsWith(".")) {
|
|
2009
|
-
if (hostname.endsWith(host)) return true;
|
|
2010
|
-
continue;
|
|
2011
|
-
}
|
|
2012
|
-
if (hostname === host) return true;
|
|
2013
|
-
if (hostname.endsWith(`.${host}`)) return true;
|
|
2014
|
-
}
|
|
2015
|
-
return false;
|
|
2016
|
-
}
|
|
2017
|
-
function getProxyEnv(url) {
|
|
2018
|
-
const noProxyRaw = process.env.NO_PROXY ?? process.env.no_proxy;
|
|
2019
|
-
if (noProxyRaw && isNoProxyMatch(url, noProxyRaw)) {
|
|
2020
|
-
return null;
|
|
2021
|
-
}
|
|
2022
|
-
if (url.protocol === "https:") {
|
|
2023
|
-
return process.env.HTTPS_PROXY ?? process.env.https_proxy ?? process.env.HTTP_PROXY ?? process.env.http_proxy ?? null;
|
|
2024
|
-
}
|
|
2025
|
-
if (url.protocol === "http:") {
|
|
2026
|
-
return process.env.HTTP_PROXY ?? process.env.http_proxy ?? null;
|
|
2027
|
-
}
|
|
2028
|
-
return null;
|
|
2029
|
-
}
|
|
2030
|
-
var runCommand = new Command6("run").description("Manage runs");
|
|
2031
|
-
runCommand.command("list").description("List runs").option("--page <page>", "Page number", "1").option("--limit <limit>", "Items per page", "50").option("--status <status>", "Filter by status").action(async (options) => {
|
|
2476
|
+
var runCommand2 = new Command6("run").description("Manage runs");
|
|
2477
|
+
runCommand2.command("list").description("List runs").option("--page <page>", "Page number", "1").option("--limit <limit>", "Items per page", "50").option("--job <jobId>", "Filter by job ID").option("--status <status>", "Filter by status").action(async (options) => {
|
|
2032
2478
|
const client = createAuthenticatedClient();
|
|
2033
2479
|
const params = {
|
|
2034
2480
|
page: options.page,
|
|
2035
2481
|
limit: options.limit
|
|
2036
2482
|
};
|
|
2483
|
+
if (options.job) params.jobId = options.job;
|
|
2037
2484
|
if (options.status) params.status = options.status;
|
|
2038
2485
|
const { data } = await withSpinner(
|
|
2039
2486
|
"Fetching runs",
|
|
@@ -2058,7 +2505,7 @@ Page ${data.pagination.page}/${data.pagination.totalPages} (${data.pagination.to
|
|
|
2058
2505
|
);
|
|
2059
2506
|
}
|
|
2060
2507
|
});
|
|
2061
|
-
|
|
2508
|
+
runCommand2.command("get <id>").description("Get run details").action(async (id) => {
|
|
2062
2509
|
const client = createAuthenticatedClient();
|
|
2063
2510
|
const { data } = await withSpinner(
|
|
2064
2511
|
"Fetching run details",
|
|
@@ -2066,7 +2513,7 @@ runCommand.command("get <id>").description("Get run details").action(async (id)
|
|
|
2066
2513
|
);
|
|
2067
2514
|
outputDetail(data);
|
|
2068
2515
|
});
|
|
2069
|
-
|
|
2516
|
+
runCommand2.command("status <id>").description("Get run status").action(async (id) => {
|
|
2070
2517
|
const client = createAuthenticatedClient();
|
|
2071
2518
|
const { data } = await withSpinner(
|
|
2072
2519
|
"Fetching run status",
|
|
@@ -2074,7 +2521,16 @@ runCommand.command("status <id>").description("Get run status").action(async (id
|
|
|
2074
2521
|
);
|
|
2075
2522
|
outputDetail(data);
|
|
2076
2523
|
});
|
|
2077
|
-
|
|
2524
|
+
runCommand2.command("permissions <id>").description("Get run access permissions").action(async (id) => {
|
|
2525
|
+
const client = createAuthenticatedClient();
|
|
2526
|
+
const { data } = await withSpinner(
|
|
2527
|
+
"Fetching run permissions",
|
|
2528
|
+
() => client.get(`/api/runs/${id}/permissions`)
|
|
2529
|
+
);
|
|
2530
|
+
const payload = data.data ?? data;
|
|
2531
|
+
outputDetail(payload);
|
|
2532
|
+
});
|
|
2533
|
+
runCommand2.command("stream <id>").description("Stream live console output from a run").option("--idle-timeout <seconds>", "Abort if no data received within this period", "60").action(async (id, options) => {
|
|
2078
2534
|
const token = requireAuth();
|
|
2079
2535
|
const baseUrl = getStoredBaseUrl() ?? "https://app.supercheck.io";
|
|
2080
2536
|
const idleTimeoutMs = Math.max(Number(options.idleTimeout) || 60, 10) * 1e3;
|
|
@@ -2092,7 +2548,7 @@ runCommand.command("stream <id>").description("Stream live console output from a
|
|
|
2092
2548
|
"Accept": "text/event-stream",
|
|
2093
2549
|
"User-Agent": `supercheck-cli/${CLI_VERSION}`
|
|
2094
2550
|
},
|
|
2095
|
-
...proxy ? { dispatcher:
|
|
2551
|
+
...proxy ? { dispatcher: getProxyAgent(proxy) } : {},
|
|
2096
2552
|
signal: controller.signal
|
|
2097
2553
|
});
|
|
2098
2554
|
clearTimeout(connectTimeout);
|
|
@@ -2182,62 +2638,101 @@ runCommand.command("stream <id>").description("Stream live console output from a
|
|
|
2182
2638
|
);
|
|
2183
2639
|
}
|
|
2184
2640
|
});
|
|
2641
|
+
runCommand2.command("cancel <id>").description("Cancel a running execution").action(async (id) => {
|
|
2642
|
+
const client = createAuthenticatedClient();
|
|
2643
|
+
const { data } = await withSpinner(
|
|
2644
|
+
"Cancelling run",
|
|
2645
|
+
() => client.post(`/api/runs/${id}/cancel`)
|
|
2646
|
+
);
|
|
2647
|
+
outputDetail(data);
|
|
2648
|
+
});
|
|
2185
2649
|
|
|
2186
2650
|
// src/commands/tests.ts
|
|
2187
2651
|
import { Command as Command7 } from "commander";
|
|
2188
|
-
import {
|
|
2652
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
2653
|
+
import { resolve as resolve7 } from "path";
|
|
2189
2654
|
import { Buffer as Buffer2 } from "buffer";
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
const existing = proxyAgents3.get(proxyUrl);
|
|
2193
|
-
if (existing) return existing;
|
|
2194
|
-
const created = new ProxyAgent3(proxyUrl);
|
|
2195
|
-
proxyAgents3.set(proxyUrl, created);
|
|
2196
|
-
return created;
|
|
2655
|
+
function normalizeTestType(input) {
|
|
2656
|
+
return normalizeTestTypeForApi(input) ?? input.trim().toLowerCase();
|
|
2197
2657
|
}
|
|
2198
|
-
function
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
if (entries.includes("*")) return true;
|
|
2203
|
-
for (const entry of entries) {
|
|
2204
|
-
const [hostPart, portPart] = entry.split(":");
|
|
2205
|
-
const host = hostPart.trim();
|
|
2206
|
-
const entryPort = portPart?.trim();
|
|
2207
|
-
if (!host) continue;
|
|
2208
|
-
if (entryPort && entryPort !== port) continue;
|
|
2209
|
-
if (host.startsWith(".")) {
|
|
2210
|
-
if (hostname.endsWith(host)) return true;
|
|
2211
|
-
continue;
|
|
2212
|
-
}
|
|
2213
|
-
if (hostname === host) return true;
|
|
2214
|
-
if (hostname.endsWith(`.${host}`)) return true;
|
|
2215
|
-
}
|
|
2216
|
-
return false;
|
|
2658
|
+
function inferTestType(filename) {
|
|
2659
|
+
if (filename.endsWith(".k6.ts") || filename.endsWith(".k6.js")) return "k6";
|
|
2660
|
+
if (filename.endsWith(".pw.ts") || filename.endsWith(".pw.js") || filename.endsWith(".spec.ts")) return "playwright";
|
|
2661
|
+
return void 0;
|
|
2217
2662
|
}
|
|
2218
|
-
function
|
|
2219
|
-
const
|
|
2220
|
-
|
|
2221
|
-
|
|
2663
|
+
function toDisplayName2(cwd, filePath) {
|
|
2664
|
+
const normalized = filePath.replace(cwd, "").replace(/^\//, "");
|
|
2665
|
+
return normalized.startsWith("_supercheck_/") ? normalized.replace(/^_supercheck_\//, "") : normalized;
|
|
2666
|
+
}
|
|
2667
|
+
function collectLocalTests(cwd, patterns, options) {
|
|
2668
|
+
if (options.file) {
|
|
2669
|
+
const filePath = resolve7(cwd, options.file);
|
|
2670
|
+
const type = inferTestType(filePath) ?? (options.type === "k6" ? "k6" : "playwright");
|
|
2671
|
+
const validationType = options.type ? normalizeTestTypeForApi(options.type) : normalizeTestTypeForApi(type);
|
|
2672
|
+
const script = readFileSync4(filePath, "utf-8");
|
|
2673
|
+
return [{
|
|
2674
|
+
path: filePath,
|
|
2675
|
+
type,
|
|
2676
|
+
name: toDisplayName2(cwd, filePath),
|
|
2677
|
+
script,
|
|
2678
|
+
validationType
|
|
2679
|
+
}];
|
|
2680
|
+
}
|
|
2681
|
+
const typeFilter = options.type?.toLowerCase();
|
|
2682
|
+
const filterType = typeFilter && ["api", "database", "custom", "browser", "playwright"].includes(typeFilter) ? "playwright" : typeFilter === "performance" || typeFilter === "load" ? "k6" : typeFilter;
|
|
2683
|
+
const files = discoverFiles(cwd, patterns).filter((file) => !filterType || file.type === filterType);
|
|
2684
|
+
return files.map((file) => {
|
|
2685
|
+
const script = readFileSync4(file.absolutePath, "utf-8");
|
|
2686
|
+
const validationType = options.type ? normalizeTestTypeForApi(options.type) : normalizeTestTypeForApi(file.type);
|
|
2687
|
+
return {
|
|
2688
|
+
path: file.absolutePath,
|
|
2689
|
+
type: file.type,
|
|
2690
|
+
name: toDisplayName2(cwd, file.absolutePath),
|
|
2691
|
+
script,
|
|
2692
|
+
validationType
|
|
2693
|
+
};
|
|
2694
|
+
});
|
|
2695
|
+
}
|
|
2696
|
+
async function validateLocalTests(client, tests) {
|
|
2697
|
+
const inputs = tests.map((test) => ({
|
|
2698
|
+
name: test.name,
|
|
2699
|
+
script: test.script,
|
|
2700
|
+
testType: test.validationType ?? normalizeTestTypeForApi(test.type)
|
|
2701
|
+
}));
|
|
2702
|
+
const results = await validateScripts(client, inputs);
|
|
2703
|
+
const failures = results.filter((r) => !r.valid);
|
|
2704
|
+
if (failures.length > 0) {
|
|
2705
|
+
const details = failures.map((f) => ` - ${f.name}: ${f.error ?? "Validation failed"}`).join("\n");
|
|
2706
|
+
throw new CLIError(`Validation failed:
|
|
2707
|
+
${details}`, 3 /* ConfigError */);
|
|
2222
2708
|
}
|
|
2223
|
-
|
|
2224
|
-
|
|
2709
|
+
const warnings = results.filter((r) => r.warnings && r.warnings.length > 0);
|
|
2710
|
+
for (const warn of warnings) {
|
|
2711
|
+
logger.warn(`Validation warnings for ${warn.name}: ${(warn.warnings ?? []).join(", ")}`);
|
|
2225
2712
|
}
|
|
2226
|
-
|
|
2227
|
-
|
|
2713
|
+
}
|
|
2714
|
+
async function runPlaywrightTests(paths, cwd, testMatch) {
|
|
2715
|
+
if (paths.length === 0) return;
|
|
2716
|
+
const tempConfig = createTempPlaywrightConfig(cwd, testMatch);
|
|
2717
|
+
try {
|
|
2718
|
+
const code = await runCommand("npx", ["playwright", "test", "--config", tempConfig.path, ...paths], cwd);
|
|
2719
|
+
if (code !== 0) {
|
|
2720
|
+
throw new CLIError(`Playwright tests failed with exit code ${code}`, 1 /* GeneralError */);
|
|
2721
|
+
}
|
|
2722
|
+
} finally {
|
|
2723
|
+
tempConfig.cleanup();
|
|
2228
2724
|
}
|
|
2229
|
-
return null;
|
|
2230
2725
|
}
|
|
2231
|
-
function
|
|
2232
|
-
const
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2726
|
+
async function runK6Tests(paths, cwd) {
|
|
2727
|
+
for (const path of paths) {
|
|
2728
|
+
const code = await runCommand("k6", ["run", path], cwd);
|
|
2729
|
+
if (code !== 0) {
|
|
2730
|
+
throw new CLIError(`k6 test failed with exit code ${code}`, 1 /* GeneralError */);
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2238
2733
|
}
|
|
2239
2734
|
var testCommand = new Command7("test").description("Manage tests");
|
|
2240
|
-
testCommand.command("list").description("List all tests").option("--page <page>", "Page number", "1").option("--limit <limit>", "Items per page", "50").option("--search <query>", "Search by title").option("--type <type>", "Filter by type (
|
|
2735
|
+
testCommand.command("list").description("List all tests").option("--page <page>", "Page number", "1").option("--limit <limit>", "Items per page", "50").option("--search <query>", "Search by title").option("--type <type>", "Filter by type (browser, performance, api, database, custom)").action(async (options) => {
|
|
2241
2736
|
const client = createAuthenticatedClient();
|
|
2242
2737
|
const params = {
|
|
2243
2738
|
page: options.page,
|
|
@@ -2269,30 +2764,32 @@ Page ${data.pagination.page}/${data.pagination.totalPages} (${data.pagination.to
|
|
|
2269
2764
|
});
|
|
2270
2765
|
testCommand.command("get <id>").description("Get test details").option("--include-script", "Include the test script content").action(async (id, options) => {
|
|
2271
2766
|
const client = createAuthenticatedClient();
|
|
2272
|
-
const params = {
|
|
2273
|
-
|
|
2767
|
+
const params = {
|
|
2768
|
+
includeScript: options.includeScript ? "true" : "false"
|
|
2769
|
+
};
|
|
2274
2770
|
const { data } = await withSpinner(
|
|
2275
2771
|
"Fetching test details",
|
|
2276
2772
|
() => client.get(`/api/tests/${id}`, params)
|
|
2277
2773
|
);
|
|
2278
2774
|
outputDetail(data);
|
|
2279
2775
|
});
|
|
2280
|
-
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 (
|
|
2281
|
-
const { readFileSync:
|
|
2282
|
-
const { resolve:
|
|
2283
|
-
const filePath =
|
|
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) => {
|
|
2777
|
+
const { readFileSync: readFileSync6 } = await import("fs");
|
|
2778
|
+
const { resolve: resolve10 } = await import("path");
|
|
2779
|
+
const filePath = resolve10(process.cwd(), options.file);
|
|
2284
2780
|
let script;
|
|
2285
2781
|
try {
|
|
2286
|
-
script =
|
|
2782
|
+
script = readFileSync6(filePath, "utf-8");
|
|
2287
2783
|
} catch {
|
|
2288
2784
|
throw new CLIError(`Cannot read file: ${filePath}`, 1 /* GeneralError */);
|
|
2289
2785
|
}
|
|
2786
|
+
const typeArg = options.type || inferTestType(options.file) || "playwright";
|
|
2290
2787
|
const client = createAuthenticatedClient();
|
|
2291
2788
|
const encodedScript = Buffer2.from(script, "utf-8").toString("base64");
|
|
2292
2789
|
const body = {
|
|
2293
2790
|
title: options.title,
|
|
2294
2791
|
script: encodedScript,
|
|
2295
|
-
type: normalizeTestType(
|
|
2792
|
+
type: normalizeTestType(typeArg)
|
|
2296
2793
|
};
|
|
2297
2794
|
if (options.description) body.description = options.description;
|
|
2298
2795
|
const { data } = await withSpinner(
|
|
@@ -2309,11 +2806,11 @@ testCommand.command("update <id>").description("Update a test").option("--title
|
|
|
2309
2806
|
if (options.title !== void 0) body.title = options.title;
|
|
2310
2807
|
if (options.description !== void 0) body.description = options.description;
|
|
2311
2808
|
if (options.file) {
|
|
2312
|
-
const { readFileSync:
|
|
2313
|
-
const { resolve:
|
|
2314
|
-
const filePath =
|
|
2809
|
+
const { readFileSync: readFileSync6 } = await import("fs");
|
|
2810
|
+
const { resolve: resolve10 } = await import("path");
|
|
2811
|
+
const filePath = resolve10(process.cwd(), options.file);
|
|
2315
2812
|
try {
|
|
2316
|
-
const raw =
|
|
2813
|
+
const raw = readFileSync6(filePath, "utf-8");
|
|
2317
2814
|
body.script = Buffer2.from(raw, "utf-8").toString("base64");
|
|
2318
2815
|
} catch {
|
|
2319
2816
|
throw new CLIError(`Cannot read file: ${filePath}`, 1 /* GeneralError */);
|
|
@@ -2346,15 +2843,67 @@ testCommand.command("delete <id>").description("Delete a test").option("--force"
|
|
|
2346
2843
|
);
|
|
2347
2844
|
logger.success(`Test ${id} deleted`);
|
|
2348
2845
|
});
|
|
2349
|
-
testCommand.command("
|
|
2846
|
+
testCommand.command("run").description("Run tests locally").option("--local", "Run locally").option("--cloud", "DEPRECATED: Cloud test execution is no longer supported from CLI").option("--file <path>", "Local test file path").option("--all", "Run all local tests").option("--type <type>", "Local test type filter (browser, performance, api, database, custom)").option("--id <id>", "DEPRECATED: Test ID (cloud execution removed)").option("--location <location>", "Execution location (k6 cloud only)").action(async (options) => {
|
|
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
|
+
}
|
|
2856
|
+
if (!options.file && !options.all) {
|
|
2857
|
+
throw new CLIError("Local runs require --file <path> or --all.", 3 /* ConfigError */);
|
|
2858
|
+
}
|
|
2859
|
+
const cwd = process.cwd();
|
|
2860
|
+
const { config } = await loadConfig({ cwd });
|
|
2350
2861
|
const client = createAuthenticatedClient();
|
|
2351
|
-
const
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2862
|
+
const patterns = {
|
|
2863
|
+
playwright: config.tests?.playwright?.testMatch,
|
|
2864
|
+
k6: config.tests?.k6?.testMatch
|
|
2865
|
+
};
|
|
2866
|
+
const localTests = collectLocalTests(cwd, patterns, {
|
|
2867
|
+
file: options.file,
|
|
2868
|
+
all: options.all,
|
|
2869
|
+
type: options.type
|
|
2870
|
+
});
|
|
2871
|
+
if (localTests.length === 0) {
|
|
2872
|
+
logger.warn("No local tests found to run.");
|
|
2873
|
+
return;
|
|
2874
|
+
}
|
|
2875
|
+
await withSpinner("Validating test scripts...", async () => {
|
|
2876
|
+
await validateLocalTests(client, localTests);
|
|
2877
|
+
}, { successText: "Test scripts validated" });
|
|
2878
|
+
const playwrightTests = localTests.filter((t) => t.type === "playwright").map((t) => t.path);
|
|
2879
|
+
const k6Tests = localTests.filter((t) => t.type === "k6").map((t) => t.path);
|
|
2880
|
+
try {
|
|
2881
|
+
if (playwrightTests.length > 0) {
|
|
2882
|
+
ensureDependenciesForTestType(cwd, "playwright");
|
|
2883
|
+
}
|
|
2884
|
+
if (k6Tests.length > 0) {
|
|
2885
|
+
ensureDependenciesForTestType(cwd, "k6");
|
|
2886
|
+
}
|
|
2887
|
+
} catch (err) {
|
|
2888
|
+
if (err instanceof DependencyError) {
|
|
2889
|
+
throw new CLIError(err.message, 3 /* ConfigError */);
|
|
2890
|
+
}
|
|
2891
|
+
throw err;
|
|
2892
|
+
}
|
|
2893
|
+
if (playwrightTests.length > 0) {
|
|
2894
|
+
logger.header("Running Playwright tests");
|
|
2895
|
+
await runPlaywrightTests(playwrightTests, cwd, config.tests?.playwright?.testMatch);
|
|
2896
|
+
}
|
|
2897
|
+
if (k6Tests.length > 0) {
|
|
2898
|
+
logger.header("Running k6 tests");
|
|
2899
|
+
await runK6Tests(k6Tests, cwd);
|
|
2900
|
+
}
|
|
2901
|
+
});
|
|
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 */
|
|
2356
2906
|
);
|
|
2357
|
-
outputDetail(data);
|
|
2358
2907
|
});
|
|
2359
2908
|
testCommand.command("tags <id>").description("Get test tags").action(async (id) => {
|
|
2360
2909
|
const client = createAuthenticatedClient();
|
|
@@ -2370,35 +2919,34 @@ testCommand.command("tags <id>").description("Get test tags").action(async (id)
|
|
|
2370
2919
|
]
|
|
2371
2920
|
});
|
|
2372
2921
|
});
|
|
2373
|
-
testCommand.command("validate").description("Validate a test script").requiredOption("--file <path>", "Path to the test script file").option("--type <type>", "Test type (
|
|
2374
|
-
const
|
|
2375
|
-
const
|
|
2376
|
-
const filePath = resolve6(process.cwd(), options.file);
|
|
2922
|
+
testCommand.command("validate").description("Validate a test script").requiredOption("--file <path>", "Path to the test script file").option("--type <type>", "Test type (browser, performance, api, database, custom)").action(async (options) => {
|
|
2923
|
+
const cwd = process.cwd();
|
|
2924
|
+
const filePath = resolve7(cwd, options.file);
|
|
2377
2925
|
let script;
|
|
2378
2926
|
try {
|
|
2379
2927
|
script = readFileSync4(filePath, "utf-8");
|
|
2380
2928
|
} catch {
|
|
2381
2929
|
throw new CLIError(`Cannot read file: ${filePath}`, 1 /* GeneralError */);
|
|
2382
2930
|
}
|
|
2931
|
+
const typeArg = options.type || inferTestType(options.file) || "playwright";
|
|
2383
2932
|
const client = createAuthenticatedClient();
|
|
2384
|
-
const
|
|
2933
|
+
const results = await withSpinner(
|
|
2385
2934
|
"Validating script",
|
|
2386
|
-
() => client
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2935
|
+
() => validateScripts(client, [{
|
|
2936
|
+
name: toDisplayName2(cwd, filePath),
|
|
2937
|
+
script,
|
|
2938
|
+
testType: normalizeTestTypeForApi(typeArg)
|
|
2939
|
+
}])
|
|
2390
2940
|
);
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
for (const err of data.errors) {
|
|
2397
|
-
logger.error(` - ${err}`);
|
|
2398
|
-
}
|
|
2941
|
+
const result = results[0];
|
|
2942
|
+
if (result?.valid) {
|
|
2943
|
+
logger.success(`Script is valid (${typeArg})`);
|
|
2944
|
+
if (result.warnings && result.warnings.length > 0) {
|
|
2945
|
+
logger.warn(`Warnings: ${result.warnings.join(", ")}`);
|
|
2399
2946
|
}
|
|
2400
|
-
|
|
2947
|
+
return;
|
|
2401
2948
|
}
|
|
2949
|
+
throw new CLIError(`Script validation failed: ${result?.error ?? "Unknown error"}`, 1 /* GeneralError */);
|
|
2402
2950
|
});
|
|
2403
2951
|
testCommand.command("status <id>").description("Stream live status events for a test").option("--idle-timeout <seconds>", "Abort if no data received within this period", "60").action(async (id, options) => {
|
|
2404
2952
|
const token = requireAuth();
|
|
@@ -2411,14 +2959,14 @@ testCommand.command("status <id>").description("Stream live status events for a
|
|
|
2411
2959
|
const controller = new AbortController();
|
|
2412
2960
|
const connectTimeout = setTimeout(() => controller.abort(), 3e4);
|
|
2413
2961
|
try {
|
|
2414
|
-
const proxy =
|
|
2962
|
+
const proxy = getProxyEnv(parsedUrl);
|
|
2415
2963
|
const response = await fetch(url, {
|
|
2416
2964
|
headers: {
|
|
2417
2965
|
"Authorization": `Bearer ${token}`,
|
|
2418
2966
|
"Accept": "text/event-stream",
|
|
2419
2967
|
"User-Agent": `supercheck-cli/${CLI_VERSION}`
|
|
2420
2968
|
},
|
|
2421
|
-
...proxy ? { dispatcher:
|
|
2969
|
+
...proxy ? { dispatcher: getProxyAgent(proxy) } : {},
|
|
2422
2970
|
signal: controller.signal
|
|
2423
2971
|
});
|
|
2424
2972
|
clearTimeout(connectTimeout);
|
|
@@ -2749,7 +3297,7 @@ tagCommand.command("delete <id>").description("Delete a tag").option("--force",
|
|
|
2749
3297
|
import { Command as Command11 } from "commander";
|
|
2750
3298
|
|
|
2751
3299
|
// src/utils/reconcile.ts
|
|
2752
|
-
import
|
|
3300
|
+
import pc4 from "picocolors";
|
|
2753
3301
|
function reconcile(local, remote) {
|
|
2754
3302
|
const changes = [];
|
|
2755
3303
|
const remoteByKey = /* @__PURE__ */ new Map();
|
|
@@ -2851,37 +3399,37 @@ function formatChangePlan(changes) {
|
|
|
2851
3399
|
logger.header("Change Plan");
|
|
2852
3400
|
logger.newline();
|
|
2853
3401
|
if (creates.length > 0) {
|
|
2854
|
-
logger.info(
|
|
3402
|
+
logger.info(pc4.green(` + ${creates.length} to create`));
|
|
2855
3403
|
for (const c of creates) {
|
|
2856
|
-
logger.info(
|
|
3404
|
+
logger.info(pc4.green(` + ${c.type}/${c.name}`));
|
|
2857
3405
|
}
|
|
2858
3406
|
}
|
|
2859
3407
|
if (updates.length > 0) {
|
|
2860
|
-
logger.info(
|
|
3408
|
+
logger.info(pc4.yellow(` ~ ${updates.length} to update`));
|
|
2861
3409
|
for (const c of updates) {
|
|
2862
|
-
logger.info(
|
|
3410
|
+
logger.info(pc4.yellow(` ~ ${c.type}/${c.name} (${c.id})`));
|
|
2863
3411
|
if (c.details) {
|
|
2864
3412
|
for (const d of c.details) {
|
|
2865
|
-
logger.info(
|
|
3413
|
+
logger.info(pc4.gray(` ${d}`));
|
|
2866
3414
|
}
|
|
2867
3415
|
}
|
|
2868
3416
|
}
|
|
2869
3417
|
}
|
|
2870
3418
|
if (deletes.length > 0) {
|
|
2871
|
-
logger.info(
|
|
3419
|
+
logger.info(pc4.red(` - ${deletes.length} to delete`));
|
|
2872
3420
|
for (const c of deletes) {
|
|
2873
|
-
logger.info(
|
|
3421
|
+
logger.info(pc4.red(` - ${c.type}/${c.name} (${c.id})`));
|
|
2874
3422
|
}
|
|
2875
3423
|
}
|
|
2876
3424
|
if (unchanged.length > 0) {
|
|
2877
|
-
logger.info(
|
|
3425
|
+
logger.info(pc4.gray(` = ${unchanged.length} unchanged`));
|
|
2878
3426
|
}
|
|
2879
3427
|
logger.newline();
|
|
2880
3428
|
const totalChanges = creates.length + updates.length + deletes.length;
|
|
2881
3429
|
if (totalChanges === 0) {
|
|
2882
3430
|
logger.success("No changes detected. Everything is in sync.");
|
|
2883
3431
|
} else {
|
|
2884
|
-
logger.info(`${
|
|
3432
|
+
logger.info(`${pc4.bold(String(totalChanges))} change(s) detected.`);
|
|
2885
3433
|
}
|
|
2886
3434
|
logger.newline();
|
|
2887
3435
|
}
|
|
@@ -2908,9 +3456,28 @@ var diffCommand = new Command11("diff").description("Preview changes between loc
|
|
|
2908
3456
|
|
|
2909
3457
|
// src/commands/deploy.ts
|
|
2910
3458
|
import { Command as Command12 } from "commander";
|
|
2911
|
-
import
|
|
3459
|
+
import { renameSync } from "fs";
|
|
3460
|
+
import { resolve as resolve8 } from "path";
|
|
3461
|
+
|
|
3462
|
+
// src/utils/paths.ts
|
|
3463
|
+
function testTypeToFolder(testType) {
|
|
3464
|
+
return testType === "k6" ? "k6" : "playwright";
|
|
3465
|
+
}
|
|
3466
|
+
function testRelativePath(testId, title, testType) {
|
|
3467
|
+
const folder = testTypeToFolder(testType);
|
|
3468
|
+
const filename = testFilename(testId, title, testType);
|
|
3469
|
+
return `_supercheck_/${folder}/${filename}`;
|
|
3470
|
+
}
|
|
3471
|
+
|
|
3472
|
+
// src/commands/deploy.ts
|
|
3473
|
+
import pc5 from "picocolors";
|
|
2912
3474
|
function prepareBodyForApi(type, body) {
|
|
2913
3475
|
const payload = { ...body };
|
|
3476
|
+
if (type === "test") {
|
|
3477
|
+
const normalizedType = normalizeTestTypeForApi(payload.testType);
|
|
3478
|
+
if (normalizedType) payload.type = normalizedType;
|
|
3479
|
+
delete payload.testType;
|
|
3480
|
+
}
|
|
2914
3481
|
if (type === "job") {
|
|
2915
3482
|
if (Array.isArray(payload.tests)) {
|
|
2916
3483
|
payload.tests = payload.tests.map((t) => {
|
|
@@ -2927,18 +3494,37 @@ function prepareBodyForApi(type, body) {
|
|
|
2927
3494
|
}
|
|
2928
3495
|
return payload;
|
|
2929
3496
|
}
|
|
2930
|
-
async function applyChange(client, change) {
|
|
3497
|
+
async function applyChange(client, change, opts) {
|
|
2931
3498
|
try {
|
|
2932
3499
|
const endpoint = getApiEndpoint(change.type);
|
|
2933
3500
|
switch (change.action) {
|
|
2934
3501
|
case "create": {
|
|
2935
|
-
const
|
|
2936
|
-
|
|
3502
|
+
const rawBody = { ...change.local.definition };
|
|
3503
|
+
delete rawBody.id;
|
|
3504
|
+
const body = prepareBodyForApi(change.type, rawBody);
|
|
3505
|
+
const response = await client.post(endpoint, body);
|
|
3506
|
+
if (change.type === "test") {
|
|
3507
|
+
const responseBody = response.data;
|
|
3508
|
+
const createdId = responseBody?.test?.id;
|
|
3509
|
+
const createdTitle = responseBody?.test?.title ?? change.local?.definition?.title;
|
|
3510
|
+
const testType = change.local?.definition?.testType === "k6" ? "k6" : "playwright";
|
|
3511
|
+
if (createdId && typeof createdTitle === "string") {
|
|
3512
|
+
const existingPath = opts.testFilesByName.get(change.name);
|
|
3513
|
+
if (existingPath) {
|
|
3514
|
+
const newRelPath = testRelativePath(createdId, createdTitle, testType);
|
|
3515
|
+
const newPath = resolve8(process.cwd(), newRelPath);
|
|
3516
|
+
if (newPath !== existingPath) {
|
|
3517
|
+
renameSync(existingPath, newPath);
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
2937
3522
|
return { success: true };
|
|
2938
3523
|
}
|
|
2939
3524
|
case "update": {
|
|
2940
3525
|
const id = change.id ?? change.remote.id;
|
|
2941
|
-
const
|
|
3526
|
+
const rawBody = { ...change.local.definition };
|
|
3527
|
+
delete rawBody.id;
|
|
2942
3528
|
const body = prepareBodyForApi(change.type, rawBody);
|
|
2943
3529
|
await client.put(`${endpoint}/${id}`, body);
|
|
2944
3530
|
return { success: true };
|
|
@@ -2977,15 +3563,45 @@ var deployCommand = new Command12("deploy").description("Push local config resou
|
|
|
2977
3563
|
changes = changes.filter((c) => c.action !== "delete");
|
|
2978
3564
|
}
|
|
2979
3565
|
const actionable = changes.filter((c) => c.action !== "no-change");
|
|
3566
|
+
const unsupportedStatusPageMutations = actionable.filter(
|
|
3567
|
+
(c) => c.type === "statusPage" && (c.action === "create" || c.action === "update")
|
|
3568
|
+
);
|
|
3569
|
+
if (unsupportedStatusPageMutations.length > 0) {
|
|
3570
|
+
throw new CLIError(
|
|
3571
|
+
"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.",
|
|
3572
|
+
3 /* ConfigError */
|
|
3573
|
+
);
|
|
3574
|
+
}
|
|
2980
3575
|
if (actionable.length === 0) {
|
|
2981
3576
|
logger.success("No changes to deploy. Everything is in sync.");
|
|
2982
3577
|
return;
|
|
2983
3578
|
}
|
|
2984
3579
|
formatChangePlan(changes);
|
|
2985
3580
|
if (options.dryRun) {
|
|
2986
|
-
logger.info(
|
|
3581
|
+
logger.info(pc5.yellow("Dry run \u2014 no changes applied."));
|
|
2987
3582
|
return;
|
|
2988
3583
|
}
|
|
3584
|
+
const testsToValidate = actionable.filter((c) => c.type === "test" && (c.action === "create" || c.action === "update"));
|
|
3585
|
+
if (testsToValidate.length > 0) {
|
|
3586
|
+
await withSpinner("Validating test scripts...", async () => {
|
|
3587
|
+
const inputs = testsToValidate.map((change) => ({
|
|
3588
|
+
name: change.name,
|
|
3589
|
+
script: String(change.local?.definition?.script ?? ""),
|
|
3590
|
+
testType: normalizeTestTypeForApi(change.local?.definition?.testType)
|
|
3591
|
+
}));
|
|
3592
|
+
const results = await validateScripts(client, inputs);
|
|
3593
|
+
const failures = results.filter((r) => !r.valid);
|
|
3594
|
+
if (failures.length > 0) {
|
|
3595
|
+
const details = failures.map((f) => ` - ${f.name}: ${f.error ?? "Validation failed"}`).join("\n");
|
|
3596
|
+
throw new CLIError(`Validation failed:
|
|
3597
|
+
${details}`, 3 /* ConfigError */);
|
|
3598
|
+
}
|
|
3599
|
+
const warnings = results.filter((r) => r.warnings && r.warnings.length > 0);
|
|
3600
|
+
for (const warn of warnings) {
|
|
3601
|
+
logger.warn(`Validation warnings for ${warn.name}: ${(warn.warnings ?? []).join(", ")}`);
|
|
3602
|
+
}
|
|
3603
|
+
}, { successText: "Test scripts validated" });
|
|
3604
|
+
}
|
|
2989
3605
|
if (!options.force) {
|
|
2990
3606
|
const confirmed = await confirmPrompt("Apply these changes?", { default: false });
|
|
2991
3607
|
if (!confirmed) {
|
|
@@ -3002,10 +3618,19 @@ var deployCommand = new Command12("deploy").description("Push local config resou
|
|
|
3002
3618
|
...actionable.filter((c) => c.action === "update"),
|
|
3003
3619
|
...actionable.filter((c) => c.action === "delete")
|
|
3004
3620
|
];
|
|
3621
|
+
const testFilesByName = /* @__PURE__ */ new Map();
|
|
3622
|
+
const patterns = {
|
|
3623
|
+
playwright: config.tests?.playwright?.testMatch,
|
|
3624
|
+
k6: config.tests?.k6?.testMatch
|
|
3625
|
+
};
|
|
3626
|
+
for (const file of discoverFiles(cwd, patterns)) {
|
|
3627
|
+
const key = file.relativePath.replace(/^_supercheck_\//, "");
|
|
3628
|
+
testFilesByName.set(key, file.absolutePath);
|
|
3629
|
+
}
|
|
3005
3630
|
for (const change of ordered) {
|
|
3006
3631
|
const actionLabel = change.action === "create" ? "+" : change.action === "update" ? "~" : "-";
|
|
3007
|
-
const color = change.action === "create" ?
|
|
3008
|
-
const result = await applyChange(client, change);
|
|
3632
|
+
const color = change.action === "create" ? pc5.green : change.action === "update" ? pc5.yellow : pc5.red;
|
|
3633
|
+
const result = await applyChange(client, change, { testFilesByName });
|
|
3009
3634
|
if (result.success) {
|
|
3010
3635
|
logger.info(color(` ${actionLabel} ${change.type}/${change.name} \u2713`));
|
|
3011
3636
|
succeeded++;
|
|
@@ -3027,7 +3652,7 @@ var deployCommand = new Command12("deploy").description("Push local config resou
|
|
|
3027
3652
|
|
|
3028
3653
|
// src/commands/destroy.ts
|
|
3029
3654
|
import { Command as Command13 } from "commander";
|
|
3030
|
-
import
|
|
3655
|
+
import pc6 from "picocolors";
|
|
3031
3656
|
async function fetchManagedResources(client, config) {
|
|
3032
3657
|
const resources = [];
|
|
3033
3658
|
const managedIds = /* @__PURE__ */ new Set();
|
|
@@ -3157,13 +3782,13 @@ var destroyCommand = new Command13("destroy").description("Tear down managed res
|
|
|
3157
3782
|
logger.header("Resources to destroy:");
|
|
3158
3783
|
logger.newline();
|
|
3159
3784
|
for (const r of managed) {
|
|
3160
|
-
logger.info(
|
|
3785
|
+
logger.info(pc6.red(` - ${r.type}/${r.name} [${r.id}]`));
|
|
3161
3786
|
}
|
|
3162
3787
|
logger.newline();
|
|
3163
|
-
logger.warn(`This will permanently delete ${
|
|
3788
|
+
logger.warn(`This will permanently delete ${pc6.bold(String(managed.length))} resource(s).`);
|
|
3164
3789
|
logger.newline();
|
|
3165
3790
|
if (options.dryRun) {
|
|
3166
|
-
logger.info(
|
|
3791
|
+
logger.info(pc6.yellow("Dry run \u2014 no resources destroyed."));
|
|
3167
3792
|
return;
|
|
3168
3793
|
}
|
|
3169
3794
|
if (!options.force) {
|
|
@@ -3182,7 +3807,7 @@ var destroyCommand = new Command13("destroy").description("Tear down managed res
|
|
|
3182
3807
|
try {
|
|
3183
3808
|
const endpoint = getApiEndpoint(resource.type);
|
|
3184
3809
|
await client.delete(`${endpoint}/${resource.id}`);
|
|
3185
|
-
logger.info(
|
|
3810
|
+
logger.info(pc6.red(` - ${resource.type}/${resource.name} \u2713`));
|
|
3186
3811
|
succeeded++;
|
|
3187
3812
|
} catch (err) {
|
|
3188
3813
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -3203,30 +3828,23 @@ var destroyCommand = new Command13("destroy").description("Tear down managed res
|
|
|
3203
3828
|
|
|
3204
3829
|
// src/commands/pull.ts
|
|
3205
3830
|
import { Command as Command14 } from "commander";
|
|
3206
|
-
import { existsSync as
|
|
3207
|
-
import { resolve as
|
|
3208
|
-
import
|
|
3831
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync2, writeFileSync as writeFileSync4, readFileSync as readFileSync5, readdirSync as readdirSync2, unlinkSync } from "fs";
|
|
3832
|
+
import { resolve as resolve9, dirname as dirname2 } from "path";
|
|
3833
|
+
import pc7 from "picocolors";
|
|
3209
3834
|
function mapTestType(apiType) {
|
|
3210
3835
|
if (apiType === "performance" || apiType === "k6") return "k6";
|
|
3211
3836
|
return "playwright";
|
|
3212
3837
|
}
|
|
3213
|
-
function testFileExtension(testType) {
|
|
3214
|
-
return testType === "k6" ? ".k6.ts" : ".pw.ts";
|
|
3215
|
-
}
|
|
3216
|
-
function testFilename(testId, testType) {
|
|
3217
|
-
const ext = testFileExtension(testType);
|
|
3218
|
-
return `${testId}${ext}`;
|
|
3219
|
-
}
|
|
3220
3838
|
function writeIfChanged(filePath, content) {
|
|
3221
|
-
if (
|
|
3222
|
-
const existing =
|
|
3839
|
+
if (existsSync5(filePath)) {
|
|
3840
|
+
const existing = readFileSync5(filePath, "utf-8");
|
|
3223
3841
|
if (existing === content) return false;
|
|
3224
3842
|
}
|
|
3225
3843
|
const dir = dirname2(filePath);
|
|
3226
|
-
if (!
|
|
3844
|
+
if (!existsSync5(dir)) {
|
|
3227
3845
|
mkdirSync2(dir, { recursive: true });
|
|
3228
3846
|
}
|
|
3229
|
-
|
|
3847
|
+
writeFileSync4(filePath, content, "utf-8");
|
|
3230
3848
|
return true;
|
|
3231
3849
|
}
|
|
3232
3850
|
function decodeScript(script) {
|
|
@@ -3238,7 +3856,11 @@ function decodeScript(script) {
|
|
|
3238
3856
|
if (reEncoded !== trimmed) {
|
|
3239
3857
|
return script;
|
|
3240
3858
|
}
|
|
3241
|
-
|
|
3859
|
+
const isTextLike = decoded.length > 0 && Array.from(decoded).every((char) => {
|
|
3860
|
+
const code = char.codePointAt(0) ?? 0;
|
|
3861
|
+
return code === 9 || code === 10 || code === 13 || code >= 32;
|
|
3862
|
+
});
|
|
3863
|
+
if (isTextLike) {
|
|
3242
3864
|
return decoded;
|
|
3243
3865
|
}
|
|
3244
3866
|
} catch {
|
|
@@ -3310,25 +3932,60 @@ async function fetchStatusPages(client) {
|
|
|
3310
3932
|
}
|
|
3311
3933
|
}
|
|
3312
3934
|
function pullTests(tests, cwd, summary) {
|
|
3313
|
-
const
|
|
3314
|
-
if (!
|
|
3315
|
-
mkdirSync2(
|
|
3935
|
+
const baseDir = resolve9(cwd, "_supercheck_");
|
|
3936
|
+
if (!existsSync5(baseDir)) {
|
|
3937
|
+
mkdirSync2(baseDir, { recursive: true });
|
|
3938
|
+
}
|
|
3939
|
+
const existingByUuid = /* @__PURE__ */ new Map();
|
|
3940
|
+
try {
|
|
3941
|
+
for (const folder of ["playwright", "k6"]) {
|
|
3942
|
+
const folderPath = resolve9(baseDir, folder);
|
|
3943
|
+
if (!existsSync5(folderPath)) continue;
|
|
3944
|
+
for (const file of readdirSync2(folderPath)) {
|
|
3945
|
+
const uuid = extractUuidFromFilename(file);
|
|
3946
|
+
if (uuid) existingByUuid.set(uuid, `${folder}/${file}`);
|
|
3947
|
+
}
|
|
3948
|
+
}
|
|
3949
|
+
} catch {
|
|
3316
3950
|
}
|
|
3317
3951
|
for (const test of tests) {
|
|
3318
3952
|
try {
|
|
3319
3953
|
const testType = mapTestType(test.type);
|
|
3320
|
-
const
|
|
3321
|
-
const
|
|
3322
|
-
|
|
3954
|
+
const relPath = testRelativePath(test.id, test.title, testType);
|
|
3955
|
+
const folderPath = resolve9(baseDir, testTypeToFolder(testType));
|
|
3956
|
+
if (!existsSync5(folderPath)) {
|
|
3957
|
+
mkdirSync2(folderPath, { recursive: true });
|
|
3958
|
+
}
|
|
3959
|
+
let script = decodeScript(test.script);
|
|
3323
3960
|
if (!script) {
|
|
3324
3961
|
logger.debug(`Skipping test "${test.title}" \u2014 no script content`);
|
|
3325
3962
|
summary.skipped++;
|
|
3326
3963
|
continue;
|
|
3327
3964
|
}
|
|
3328
|
-
|
|
3965
|
+
if (/@title\s+/.test(script)) {
|
|
3966
|
+
script = script.replace(/(@title\s+)(.+?)(\r?\n|\*\/|$)/, `$1${test.title}$3`);
|
|
3967
|
+
} else if (/\/\*\*([\s\S]*?)\*\//.test(script)) {
|
|
3968
|
+
script = script.replace(/(\/\*\*)/, `$1
|
|
3969
|
+
* @title ${test.title}`);
|
|
3970
|
+
} else {
|
|
3971
|
+
script = `// @title ${test.title}
|
|
3972
|
+
|
|
3973
|
+
${script}`;
|
|
3974
|
+
}
|
|
3975
|
+
const existingFilename = existingByUuid.get(test.id);
|
|
3976
|
+
const currentFile = relPath.replace(/^_supercheck_\//, "");
|
|
3977
|
+
if (existingFilename && existingFilename !== currentFile) {
|
|
3978
|
+
const oldPath = resolve9(baseDir, existingFilename);
|
|
3979
|
+
try {
|
|
3980
|
+
unlinkSync(oldPath);
|
|
3981
|
+
logger.debug(` Renamed: ${existingFilename} \u2192 ${currentFile}`);
|
|
3982
|
+
} catch {
|
|
3983
|
+
}
|
|
3984
|
+
}
|
|
3985
|
+
const filePath = resolve9(cwd, relPath);
|
|
3329
3986
|
const written = writeIfChanged(filePath, script);
|
|
3330
3987
|
if (written) {
|
|
3331
|
-
logger.info(
|
|
3988
|
+
logger.info(pc7.green(` + ${relPath}`));
|
|
3332
3989
|
summary.tests++;
|
|
3333
3990
|
} else {
|
|
3334
3991
|
logger.debug(` = ${relPath} (unchanged)`);
|
|
@@ -3483,10 +4140,10 @@ function generateConfigContent(opts) {
|
|
|
3483
4140
|
parts.push(" // Test file patterns \u2014 Playwright (.pw.ts) and k6 (.k6.ts) scripts");
|
|
3484
4141
|
parts.push(" tests: {");
|
|
3485
4142
|
parts.push(" playwright: {");
|
|
3486
|
-
parts.push(` testMatch: '_supercheck_/
|
|
4143
|
+
parts.push(` testMatch: '_supercheck_/playwright/**/*.pw.ts',`);
|
|
3487
4144
|
parts.push(" },");
|
|
3488
4145
|
parts.push(" k6: {");
|
|
3489
|
-
parts.push(` testMatch: '_supercheck_/
|
|
4146
|
+
parts.push(` testMatch: '_supercheck_/k6/**/*.k6.ts',`);
|
|
3490
4147
|
parts.push(" },");
|
|
3491
4148
|
parts.push(" },");
|
|
3492
4149
|
if (opts.monitors.length > 0) {
|
|
@@ -3529,7 +4186,9 @@ function generateConfigContent(opts) {
|
|
|
3529
4186
|
parts.push(" * npm install -D @supercheck/cli typescript @types/node");
|
|
3530
4187
|
parts.push(" * # If using Playwright tests, also install:");
|
|
3531
4188
|
parts.push(" * npm install -D @playwright/test");
|
|
3532
|
-
parts.push(" * # If using k6 tests, install k6:
|
|
4189
|
+
parts.push(" * # If using k6 tests, also install types and k6 runtime:");
|
|
4190
|
+
parts.push(" * npm install -D @types/k6");
|
|
4191
|
+
parts.push(" * # Install k6 runtime: https://grafana.com/docs/k6/latest/set-up/install-k6/");
|
|
3533
4192
|
parts.push(" *");
|
|
3534
4193
|
parts.push(" * 2. Review the configuration above and make any changes you need.");
|
|
3535
4194
|
parts.push(" * 3. Preview what will change: npx supercheck diff");
|
|
@@ -3544,7 +4203,7 @@ function generateConfigContent(opts) {
|
|
|
3544
4203
|
parts.push(" *");
|
|
3545
4204
|
parts.push(" * supercheck test list List all tests");
|
|
3546
4205
|
parts.push(" * supercheck test validate Validate a local test script");
|
|
3547
|
-
parts.push(" * supercheck test
|
|
4206
|
+
parts.push(" * supercheck test run --local Run local test scripts");
|
|
3548
4207
|
parts.push(" *");
|
|
3549
4208
|
parts.push(" * supercheck monitor list List all monitors");
|
|
3550
4209
|
parts.push(" * supercheck job list List all jobs");
|
|
@@ -3654,39 +4313,39 @@ var pullCommand = new Command14("pull").description("Pull tests, monitors, jobs,
|
|
|
3654
4313
|
logger.header("Resources that would be pulled:");
|
|
3655
4314
|
logger.newline();
|
|
3656
4315
|
if (tests.length > 0) {
|
|
3657
|
-
logger.info(
|
|
4316
|
+
logger.info(pc7.cyan(` Tests (${tests.length}):`));
|
|
3658
4317
|
for (const t of tests) {
|
|
3659
4318
|
const testType = mapTestType(t.type);
|
|
3660
4319
|
logger.info(` ${t.title} (${testType})`);
|
|
3661
4320
|
}
|
|
3662
4321
|
}
|
|
3663
4322
|
if (monitors.length > 0) {
|
|
3664
|
-
logger.info(
|
|
4323
|
+
logger.info(pc7.cyan(` Monitors (${monitors.length}):`));
|
|
3665
4324
|
for (const m of monitors) logger.info(` ${m.name} (${m.type}, every ${m.frequencyMinutes ?? "?"}min)`);
|
|
3666
4325
|
}
|
|
3667
4326
|
if (jobs.length > 0) {
|
|
3668
|
-
logger.info(
|
|
4327
|
+
logger.info(pc7.cyan(` Jobs (${jobs.length}):`));
|
|
3669
4328
|
for (const j of jobs) logger.info(` ${j.name}${j.cronSchedule ? ` (${j.cronSchedule})` : ""}`);
|
|
3670
4329
|
}
|
|
3671
4330
|
if (variables.length > 0) {
|
|
3672
|
-
logger.info(
|
|
4331
|
+
logger.info(pc7.cyan(` Variables (${variables.length}):`));
|
|
3673
4332
|
for (const v of variables) logger.info(` ${v.key}${v.isSecret ? " (secret)" : ""}`);
|
|
3674
4333
|
}
|
|
3675
4334
|
if (tags.length > 0) {
|
|
3676
|
-
logger.info(
|
|
4335
|
+
logger.info(pc7.cyan(` Tags (${tags.length}):`));
|
|
3677
4336
|
for (const t of tags) logger.info(` ${t.name}`);
|
|
3678
4337
|
}
|
|
3679
4338
|
if (statusPages.length > 0) {
|
|
3680
|
-
logger.info(
|
|
4339
|
+
logger.info(pc7.cyan(` Status Pages (${statusPages.length}):`));
|
|
3681
4340
|
for (const sp of statusPages) logger.info(` ${sp.name} (${sp.status ?? "draft"})`);
|
|
3682
4341
|
}
|
|
3683
4342
|
logger.newline();
|
|
3684
|
-
logger.info(
|
|
4343
|
+
logger.info(pc7.yellow("Dry run \u2014 no files written."));
|
|
3685
4344
|
logger.newline();
|
|
3686
4345
|
return;
|
|
3687
4346
|
}
|
|
3688
4347
|
if (!options.force) {
|
|
3689
|
-
logger.info(`Found ${
|
|
4348
|
+
logger.info(`Found ${pc7.bold(String(totalResources))} resources to pull.`);
|
|
3690
4349
|
logger.info("This will write test scripts and update supercheck.config.ts.");
|
|
3691
4350
|
logger.newline();
|
|
3692
4351
|
const { confirmPrompt: confirmPrompt2 } = await import("../prompt-BPDPYRS7.js");
|
|
@@ -3733,11 +4392,52 @@ var pullCommand = new Command14("pull").description("Pull tests, monitors, jobs,
|
|
|
3733
4392
|
pullTests(fullTests, cwd, summary);
|
|
3734
4393
|
logger.newline();
|
|
3735
4394
|
}
|
|
4395
|
+
const pkgPath = resolve9(cwd, "package.json");
|
|
4396
|
+
if (!options.dryRun && !existsSync5(pkgPath)) {
|
|
4397
|
+
logger.newline();
|
|
4398
|
+
logger.info("Initializing project dependencies...");
|
|
4399
|
+
const pm = detectPackageManager(cwd);
|
|
4400
|
+
ensurePackageJson(cwd);
|
|
4401
|
+
const packages = ["@supercheck/cli", "typescript", "@types/node"];
|
|
4402
|
+
const hasPlaywright = tests.some((t) => mapTestType(t.type) === "playwright");
|
|
4403
|
+
const hasK6 = tests.some((t) => mapTestType(t.type) === "k6");
|
|
4404
|
+
if (hasPlaywright) packages.push("@playwright/test");
|
|
4405
|
+
if (hasK6) packages.push("@types/k6");
|
|
4406
|
+
await installDependencies(cwd, pm, {
|
|
4407
|
+
packages,
|
|
4408
|
+
skipInstall: false
|
|
4409
|
+
});
|
|
4410
|
+
const tsconfigPath = resolve9(cwd, "tsconfig.supercheck.json");
|
|
4411
|
+
if (!existsSync5(tsconfigPath)) {
|
|
4412
|
+
const tsconfigContent = JSON.stringify({
|
|
4413
|
+
compilerOptions: {
|
|
4414
|
+
target: "ES2022",
|
|
4415
|
+
module: "ESNext",
|
|
4416
|
+
moduleResolution: "bundler",
|
|
4417
|
+
esModuleInterop: true,
|
|
4418
|
+
strict: true,
|
|
4419
|
+
skipLibCheck: true,
|
|
4420
|
+
resolveJsonModule: true,
|
|
4421
|
+
isolatedModules: true,
|
|
4422
|
+
noEmit: true,
|
|
4423
|
+
types: ["node", ...hasK6 ? ["k6"] : []]
|
|
4424
|
+
},
|
|
4425
|
+
include: [
|
|
4426
|
+
"supercheck.config.ts",
|
|
4427
|
+
"supercheck.config.local.ts",
|
|
4428
|
+
"_supercheck_/**/*.ts"
|
|
4429
|
+
]
|
|
4430
|
+
}, null, 2) + "\n";
|
|
4431
|
+
writeFileSync4(tsconfigPath, tsconfigContent, "utf-8");
|
|
4432
|
+
logger.success("Created tsconfig.supercheck.json (IDE IntelliSense)");
|
|
4433
|
+
}
|
|
4434
|
+
logger.success("Project initialized with dependencies");
|
|
4435
|
+
}
|
|
3736
4436
|
if (!options.testsOnly) {
|
|
3737
4437
|
const testMap = /* @__PURE__ */ new Map();
|
|
3738
4438
|
for (const t of tests) {
|
|
3739
4439
|
const testType = mapTestType(t.type);
|
|
3740
|
-
const filePath =
|
|
4440
|
+
const filePath = testRelativePath(t.id, t.title, testType);
|
|
3741
4441
|
testMap.set(t.id, filePath);
|
|
3742
4442
|
}
|
|
3743
4443
|
const monitorDefs = buildMonitorDefinitions(monitors);
|
|
@@ -3766,15 +4466,15 @@ var pullCommand = new Command14("pull").description("Pull tests, monitors, jobs,
|
|
|
3766
4466
|
tags: tagDefs,
|
|
3767
4467
|
statusPages: statusPageDefs
|
|
3768
4468
|
});
|
|
3769
|
-
const configPath =
|
|
4469
|
+
const configPath = resolve9(cwd, "supercheck.config.ts");
|
|
3770
4470
|
const configChanged = writeIfChanged(configPath, configContent);
|
|
3771
4471
|
if (configChanged) {
|
|
3772
4472
|
logger.header("Config:");
|
|
3773
|
-
if (monitorDefs.length > 0) logger.info(
|
|
3774
|
-
if (jobDefs.length > 0) logger.info(
|
|
3775
|
-
if (variableDefs.length > 0) logger.info(
|
|
3776
|
-
if (tagDefs.length > 0) logger.info(
|
|
3777
|
-
if (statusPageDefs.length > 0) logger.info(
|
|
4473
|
+
if (monitorDefs.length > 0) logger.info(pc7.green(` + ${monitorDefs.length} monitor(s)`));
|
|
4474
|
+
if (jobDefs.length > 0) logger.info(pc7.green(` + ${jobDefs.length} job(s)`));
|
|
4475
|
+
if (variableDefs.length > 0) logger.info(pc7.green(` + ${variableDefs.length} variable(s)`));
|
|
4476
|
+
if (tagDefs.length > 0) logger.info(pc7.green(` + ${tagDefs.length} tag(s)`));
|
|
4477
|
+
if (statusPageDefs.length > 0) logger.info(pc7.green(` + ${statusPageDefs.length} status page(s)`));
|
|
3778
4478
|
logger.success("Updated supercheck.config.ts");
|
|
3779
4479
|
} else {
|
|
3780
4480
|
logger.info("supercheck.config.ts is already up to date");
|
|
@@ -3815,9 +4515,53 @@ var pullCommand = new Command14("pull").description("Pull tests, monitors, jobs,
|
|
|
3815
4515
|
logger.newline();
|
|
3816
4516
|
});
|
|
3817
4517
|
|
|
3818
|
-
// src/commands/
|
|
4518
|
+
// src/commands/validate.ts
|
|
3819
4519
|
import { Command as Command15 } from "commander";
|
|
3820
|
-
|
|
4520
|
+
import pc8 from "picocolors";
|
|
4521
|
+
var validateCommand = new Command15("validate").description("Validate local test scripts against Supercheck rules").option("--config <path>", "Path to config file").action(async (options) => {
|
|
4522
|
+
const cwd = process.cwd();
|
|
4523
|
+
const { config } = await loadConfig({ cwd, configPath: options.config });
|
|
4524
|
+
const client = createAuthenticatedClient();
|
|
4525
|
+
const localResources = buildLocalResources(config, cwd);
|
|
4526
|
+
const tests = localResources.filter((r) => r.type === "test");
|
|
4527
|
+
if (tests.length === 0) {
|
|
4528
|
+
logger.warn("No local tests found to validate.");
|
|
4529
|
+
return;
|
|
4530
|
+
}
|
|
4531
|
+
const inputs = tests.map((test) => ({
|
|
4532
|
+
name: test.name,
|
|
4533
|
+
script: String(test.definition?.script ?? ""),
|
|
4534
|
+
testType: normalizeTestTypeForApi(test.definition?.testType)
|
|
4535
|
+
}));
|
|
4536
|
+
const results = await withSpinner("Validating test scripts...", async () => {
|
|
4537
|
+
return validateScripts(client, inputs);
|
|
4538
|
+
}, { successText: "Validation complete" });
|
|
4539
|
+
logger.newline();
|
|
4540
|
+
logger.header("Validation Results");
|
|
4541
|
+
logger.newline();
|
|
4542
|
+
let failed = 0;
|
|
4543
|
+
for (const result of results) {
|
|
4544
|
+
if (result.valid) {
|
|
4545
|
+
logger.info(pc8.green(` \u2713 ${result.name}`));
|
|
4546
|
+
if (result.warnings && result.warnings.length > 0) {
|
|
4547
|
+
logger.info(pc8.yellow(` Warnings: ${result.warnings.join(", ")}`));
|
|
4548
|
+
}
|
|
4549
|
+
} else {
|
|
4550
|
+
failed++;
|
|
4551
|
+
logger.info(pc8.red(` \u2717 ${result.name}`));
|
|
4552
|
+
logger.info(pc8.gray(` ${result.error ?? "Validation failed"}`));
|
|
4553
|
+
}
|
|
4554
|
+
}
|
|
4555
|
+
logger.newline();
|
|
4556
|
+
if (failed > 0) {
|
|
4557
|
+
throw new CLIError(`Validation failed for ${failed} test(s).`, 3 /* ConfigError */);
|
|
4558
|
+
}
|
|
4559
|
+
logger.success("All tests passed validation.");
|
|
4560
|
+
});
|
|
4561
|
+
|
|
4562
|
+
// src/commands/notifications.ts
|
|
4563
|
+
import { Command as Command16 } from "commander";
|
|
4564
|
+
var notificationCommand = new Command16("notification").alias("notifications").description("Manage notification providers");
|
|
3821
4565
|
notificationCommand.command("list").description("List notification providers").action(async () => {
|
|
3822
4566
|
const client = createAuthenticatedClient();
|
|
3823
4567
|
const { data } = await withSpinner(
|
|
@@ -3949,8 +4693,8 @@ notificationCommand.command("test").description("Send a test notification to ver
|
|
|
3949
4693
|
});
|
|
3950
4694
|
|
|
3951
4695
|
// src/commands/alerts.ts
|
|
3952
|
-
import { Command as
|
|
3953
|
-
var alertCommand = new
|
|
4696
|
+
import { Command as Command17 } from "commander";
|
|
4697
|
+
var alertCommand = new Command17("alert").alias("alerts").description("View alert history");
|
|
3954
4698
|
alertCommand.command("history").description("Get alert history").option("--page <page>", "Page number", "1").option("--limit <limit>", "Number of results per page", "50").action(async (options) => {
|
|
3955
4699
|
const client = createAuthenticatedClient();
|
|
3956
4700
|
const { data } = await withSpinner(
|
|
@@ -3978,8 +4722,8 @@ Page ${pagination.page}/${pagination.totalPages} (${pagination.total} total)`
|
|
|
3978
4722
|
});
|
|
3979
4723
|
|
|
3980
4724
|
// src/commands/audit.ts
|
|
3981
|
-
import { Command as
|
|
3982
|
-
var auditCommand = new
|
|
4725
|
+
import { Command as Command18 } from "commander";
|
|
4726
|
+
var auditCommand = new Command18("audit").description("View audit logs (admin)").option("--page <page>", "Page number", "1").option("--limit <limit>", "Items per page", "20").option("--search <query>", "Search by action").option("--action <action>", "Filter by action type").action(async (options) => {
|
|
3983
4727
|
const client = createAuthenticatedClient();
|
|
3984
4728
|
const params = {
|
|
3985
4729
|
page: options.page,
|
|
@@ -4017,8 +4761,227 @@ Page ${pagination.currentPage}/${pagination.totalPages} (${pagination.totalCount
|
|
|
4017
4761
|
}
|
|
4018
4762
|
});
|
|
4019
4763
|
|
|
4764
|
+
// src/commands/doctor.ts
|
|
4765
|
+
import { Command as Command19 } from "commander";
|
|
4766
|
+
import pc9 from "picocolors";
|
|
4767
|
+
var doctorCommand = new Command19("doctor").description("Check that all dependencies and configuration are set up correctly").option("--fix", "Attempt to automatically fix missing dependencies").action(async (options) => {
|
|
4768
|
+
const cwd = process.cwd();
|
|
4769
|
+
const format = getOutputFormat();
|
|
4770
|
+
logger.newline();
|
|
4771
|
+
logger.header("Supercheck Doctor");
|
|
4772
|
+
logger.newline();
|
|
4773
|
+
logger.info(pc9.bold("Dependencies:"));
|
|
4774
|
+
const deps = checkAllDependencies(cwd);
|
|
4775
|
+
if (format === "json") {
|
|
4776
|
+
output(deps, {
|
|
4777
|
+
columns: [
|
|
4778
|
+
{ key: "name", header: "Dependency" },
|
|
4779
|
+
{ key: "installed", header: "Installed" },
|
|
4780
|
+
{ key: "version", header: "Version" },
|
|
4781
|
+
{ key: "required", header: "Required" },
|
|
4782
|
+
{ key: "installHint", header: "Install" }
|
|
4783
|
+
]
|
|
4784
|
+
});
|
|
4785
|
+
} else {
|
|
4786
|
+
logger.output(formatDependencyReport(deps));
|
|
4787
|
+
}
|
|
4788
|
+
logger.newline();
|
|
4789
|
+
logger.info(pc9.bold("Authentication:"));
|
|
4790
|
+
const hasAuth = isAuthenticated();
|
|
4791
|
+
if (hasAuth) {
|
|
4792
|
+
const baseUrl = getStoredBaseUrl() ?? "https://app.supercheck.io";
|
|
4793
|
+
logger.output(` ${pc9.green("\u2713")} Authenticated (${pc9.dim(baseUrl)})`);
|
|
4794
|
+
} else {
|
|
4795
|
+
logger.output(` ${pc9.yellow("\u25CB")} Not authenticated \u2014 run: supercheck login --token <token>`);
|
|
4796
|
+
}
|
|
4797
|
+
logger.newline();
|
|
4798
|
+
logger.info(pc9.bold("Configuration:"));
|
|
4799
|
+
const configResult = await tryLoadConfig();
|
|
4800
|
+
if (configResult) {
|
|
4801
|
+
logger.output(` ${pc9.green("\u2713")} supercheck.config.ts found`);
|
|
4802
|
+
const org = configResult.config.project?.organization;
|
|
4803
|
+
const proj = configResult.config.project?.project;
|
|
4804
|
+
if (org && proj) {
|
|
4805
|
+
logger.output(` ${pc9.green("\u2713")} Project: ${pc9.dim(`${org}/${proj}`)}`);
|
|
4806
|
+
} else {
|
|
4807
|
+
logger.output(` ${pc9.yellow("\u25CB")} Project org/project not configured in supercheck.config.ts`);
|
|
4808
|
+
}
|
|
4809
|
+
} else {
|
|
4810
|
+
logger.output(` ${pc9.yellow("\u25CB")} No supercheck.config.ts \u2014 run: supercheck init`);
|
|
4811
|
+
}
|
|
4812
|
+
logger.newline();
|
|
4813
|
+
const missingRequired = deps.filter((d) => !d.installed && d.required);
|
|
4814
|
+
if (options.fix && missingRequired.length > 0) {
|
|
4815
|
+
logger.header("Attempting to fix missing dependencies...");
|
|
4816
|
+
logger.newline();
|
|
4817
|
+
for (const dep of missingRequired) {
|
|
4818
|
+
if (dep.name === "Playwright browsers") {
|
|
4819
|
+
const ok = await installPlaywrightBrowsers(cwd);
|
|
4820
|
+
if (ok) {
|
|
4821
|
+
logger.success("Playwright browsers installed");
|
|
4822
|
+
} else {
|
|
4823
|
+
logger.error(`Failed to install Playwright browsers. Run manually: ${dep.installHint}`);
|
|
4824
|
+
}
|
|
4825
|
+
} else if (dep.name === "@playwright/test") {
|
|
4826
|
+
logger.info(`Install @playwright/test:`);
|
|
4827
|
+
logger.info(` ${dep.installHint}`);
|
|
4828
|
+
}
|
|
4829
|
+
}
|
|
4830
|
+
logger.newline();
|
|
4831
|
+
}
|
|
4832
|
+
if (missingRequired.length === 0 && hasAuth && configResult) {
|
|
4833
|
+
logger.success("Everything looks good! You're ready to use Supercheck.");
|
|
4834
|
+
} else {
|
|
4835
|
+
const issues = [];
|
|
4836
|
+
if (missingRequired.length > 0) {
|
|
4837
|
+
issues.push(`${missingRequired.length} missing required dependency(s)`);
|
|
4838
|
+
}
|
|
4839
|
+
if (!hasAuth) {
|
|
4840
|
+
issues.push("not authenticated");
|
|
4841
|
+
}
|
|
4842
|
+
if (!configResult) {
|
|
4843
|
+
issues.push("no config file");
|
|
4844
|
+
}
|
|
4845
|
+
logger.warn(`Issues found: ${issues.join(", ")}`);
|
|
4846
|
+
if (!options.fix && missingRequired.length > 0) {
|
|
4847
|
+
logger.info(`Run ${pc9.bold("supercheck doctor --fix")} to attempt automatic fixes.`);
|
|
4848
|
+
}
|
|
4849
|
+
}
|
|
4850
|
+
logger.newline();
|
|
4851
|
+
if (missingRequired.length > 0) {
|
|
4852
|
+
throw new CLIError(
|
|
4853
|
+
`Missing ${missingRequired.length} required dependency(s). Run 'supercheck doctor --fix' or install manually.`,
|
|
4854
|
+
3 /* ConfigError */
|
|
4855
|
+
);
|
|
4856
|
+
}
|
|
4857
|
+
});
|
|
4858
|
+
|
|
4859
|
+
// src/commands/upgrade.ts
|
|
4860
|
+
import { Command as Command20 } from "commander";
|
|
4861
|
+
import { spawn as spawn2 } from "child_process";
|
|
4862
|
+
var PACKAGE_NAME = "@supercheck/cli";
|
|
4863
|
+
var TAG_PATTERN = /^[a-z0-9][a-z0-9._-]*$/i;
|
|
4864
|
+
function normalizeTag(tag) {
|
|
4865
|
+
const trimmed = tag.trim();
|
|
4866
|
+
if (trimmed.startsWith("v") && trimmed.length > 1 && /\d/.test(trimmed[1])) {
|
|
4867
|
+
return trimmed.slice(1);
|
|
4868
|
+
}
|
|
4869
|
+
return trimmed;
|
|
4870
|
+
}
|
|
4871
|
+
function getDefaultTag() {
|
|
4872
|
+
return "latest";
|
|
4873
|
+
}
|
|
4874
|
+
function detectPackageManager2() {
|
|
4875
|
+
const userAgent = process.env.npm_config_user_agent ?? "";
|
|
4876
|
+
if (userAgent.includes("pnpm")) return "pnpm";
|
|
4877
|
+
if (userAgent.includes("yarn")) return "yarn";
|
|
4878
|
+
if (userAgent.includes("bun")) return "bun";
|
|
4879
|
+
return "npm";
|
|
4880
|
+
}
|
|
4881
|
+
function buildInstallCommand(pm, pkgSpec) {
|
|
4882
|
+
switch (pm) {
|
|
4883
|
+
case "yarn": {
|
|
4884
|
+
const args = ["global", "add", pkgSpec];
|
|
4885
|
+
return { command: "yarn", args, preview: `yarn ${args.join(" ")}` };
|
|
4886
|
+
}
|
|
4887
|
+
case "pnpm": {
|
|
4888
|
+
const args = ["add", "-g", pkgSpec];
|
|
4889
|
+
return { command: "pnpm", args, preview: `pnpm ${args.join(" ")}` };
|
|
4890
|
+
}
|
|
4891
|
+
case "bun": {
|
|
4892
|
+
const args = ["add", "-g", pkgSpec];
|
|
4893
|
+
return { command: "bun", args, preview: `bun ${args.join(" ")}` };
|
|
4894
|
+
}
|
|
4895
|
+
case "npm":
|
|
4896
|
+
default: {
|
|
4897
|
+
const args = ["install", "-g", pkgSpec];
|
|
4898
|
+
return { command: "npm", args, preview: `npm ${args.join(" ")}` };
|
|
4899
|
+
}
|
|
4900
|
+
}
|
|
4901
|
+
}
|
|
4902
|
+
function runInstallCommand(command, args) {
|
|
4903
|
+
return new Promise((resolve10, reject) => {
|
|
4904
|
+
const child = spawn2(command, args, { stdio: "inherit" });
|
|
4905
|
+
child.on("error", (err) => reject(err));
|
|
4906
|
+
child.on("close", (code) => resolve10(code ?? 0));
|
|
4907
|
+
});
|
|
4908
|
+
}
|
|
4909
|
+
var upgradeCommand = new Command20("upgrade").description("Upgrade the Supercheck CLI to the latest version").option("--tag <tag>", "NPM dist-tag or version (latest, next, canary, 0.1.1-beta.1)").option("--package-manager <pm>", "Package manager to use (npm, yarn, pnpm, bun)").option("-y, --yes", "Skip confirmation prompt").option("--dry-run", "Print the upgrade command without running it").action(async (options) => {
|
|
4910
|
+
const format = getOutputFormat();
|
|
4911
|
+
const tag = normalizeTag(options.tag ?? getDefaultTag());
|
|
4912
|
+
if (!TAG_PATTERN.test(tag)) {
|
|
4913
|
+
throw new CLIError(
|
|
4914
|
+
`Invalid tag "${tag}". Use a valid dist-tag (latest, next, canary) or version (0.1.1-beta.1).`,
|
|
4915
|
+
3 /* ConfigError */
|
|
4916
|
+
);
|
|
4917
|
+
}
|
|
4918
|
+
const pm = options.packageManager ?? detectPackageManager2();
|
|
4919
|
+
if (!["npm", "yarn", "pnpm", "bun"].includes(pm)) {
|
|
4920
|
+
throw new CLIError(
|
|
4921
|
+
`Unsupported package manager "${pm}". Use npm, yarn, pnpm, or bun.`,
|
|
4922
|
+
3 /* ConfigError */
|
|
4923
|
+
);
|
|
4924
|
+
}
|
|
4925
|
+
const pkgSpec = `${PACKAGE_NAME}@${tag}`;
|
|
4926
|
+
const install = buildInstallCommand(pm, pkgSpec);
|
|
4927
|
+
if (options.dryRun) {
|
|
4928
|
+
const payload = {
|
|
4929
|
+
action: "upgrade",
|
|
4930
|
+
package: PACKAGE_NAME,
|
|
4931
|
+
tag,
|
|
4932
|
+
packageManager: pm,
|
|
4933
|
+
command: install.preview,
|
|
4934
|
+
dryRun: true
|
|
4935
|
+
};
|
|
4936
|
+
if (format === "json") {
|
|
4937
|
+
output(payload);
|
|
4938
|
+
} else {
|
|
4939
|
+
logger.info(`Upgrade command: ${install.preview}`);
|
|
4940
|
+
}
|
|
4941
|
+
return;
|
|
4942
|
+
}
|
|
4943
|
+
if (!options.yes) {
|
|
4944
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
4945
|
+
throw new CLIError(
|
|
4946
|
+
"Upgrade requires confirmation. Re-run with --yes to skip the prompt.",
|
|
4947
|
+
3 /* ConfigError */
|
|
4948
|
+
);
|
|
4949
|
+
}
|
|
4950
|
+
const confirmed = await confirmPrompt(`Upgrade Supercheck CLI using ${pm}?`, { default: false });
|
|
4951
|
+
if (!confirmed) {
|
|
4952
|
+
logger.info("Upgrade cancelled.");
|
|
4953
|
+
return;
|
|
4954
|
+
}
|
|
4955
|
+
}
|
|
4956
|
+
let code = 0;
|
|
4957
|
+
try {
|
|
4958
|
+
code = await withSpinner(
|
|
4959
|
+
"Upgrading Supercheck CLI...",
|
|
4960
|
+
() => runInstallCommand(install.command, install.args)
|
|
4961
|
+
);
|
|
4962
|
+
} catch (err) {
|
|
4963
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4964
|
+
throw new CLIError(`Failed to run upgrade command: ${message}`, 1 /* GeneralError */);
|
|
4965
|
+
}
|
|
4966
|
+
if (code !== 0) {
|
|
4967
|
+
throw new CLIError(`Upgrade failed with exit code ${code}.`, 1 /* GeneralError */);
|
|
4968
|
+
}
|
|
4969
|
+
if (format === "json") {
|
|
4970
|
+
output({
|
|
4971
|
+
action: "upgrade",
|
|
4972
|
+
package: PACKAGE_NAME,
|
|
4973
|
+
tag,
|
|
4974
|
+
packageManager: pm,
|
|
4975
|
+
command: install.preview,
|
|
4976
|
+
success: true
|
|
4977
|
+
});
|
|
4978
|
+
return;
|
|
4979
|
+
}
|
|
4980
|
+
logger.success(`Supercheck CLI upgraded (${pkgSpec}).`);
|
|
4981
|
+
});
|
|
4982
|
+
|
|
4020
4983
|
// src/bin/supercheck.ts
|
|
4021
|
-
var program = new
|
|
4984
|
+
var program = new Command21().name("supercheck").description("Open-source testing, monitoring, and reliability \u2014 as code").version(CLI_VERSION, "-v, --version").option("--json", "Output in JSON format").option("--quiet", "Suppress non-essential output").option("--debug", "Enable debug logging").hook("preAction", (_thisCommand, actionCommand) => {
|
|
4022
4985
|
const opts = program.opts();
|
|
4023
4986
|
if (opts.json) {
|
|
4024
4987
|
setOutputFormat("json");
|
|
@@ -4038,13 +5001,14 @@ program.addCommand(whoamiCommand);
|
|
|
4038
5001
|
program.addCommand(initCommand);
|
|
4039
5002
|
program.addCommand(configCommand);
|
|
4040
5003
|
program.addCommand(jobCommand);
|
|
4041
|
-
program.addCommand(
|
|
5004
|
+
program.addCommand(runCommand2);
|
|
4042
5005
|
program.addCommand(testCommand);
|
|
4043
5006
|
program.addCommand(monitorCommand);
|
|
4044
5007
|
program.addCommand(varCommand);
|
|
4045
5008
|
program.addCommand(tagCommand);
|
|
4046
5009
|
program.addCommand(diffCommand);
|
|
4047
5010
|
program.addCommand(deployCommand);
|
|
5011
|
+
program.addCommand(validateCommand);
|
|
4048
5012
|
program.addCommand(destroyCommand);
|
|
4049
5013
|
program.addCommand(pullCommand);
|
|
4050
5014
|
program.addCommand(notificationCommand);
|
|
@@ -4052,14 +5016,42 @@ program.addCommand(alertCommand);
|
|
|
4052
5016
|
program.addCommand(auditCommand);
|
|
4053
5017
|
program.addCommand(healthCommand);
|
|
4054
5018
|
program.addCommand(locationsCommand);
|
|
5019
|
+
program.addCommand(doctorCommand);
|
|
5020
|
+
program.addCommand(upgradeCommand);
|
|
4055
5021
|
program.exitOverride();
|
|
4056
5022
|
async function main() {
|
|
4057
5023
|
try {
|
|
4058
5024
|
await program.parseAsync(process.argv);
|
|
4059
5025
|
} catch (err) {
|
|
5026
|
+
if (err instanceof ApiRequestError && err.responseBody !== void 0) {
|
|
5027
|
+
const details = (() => {
|
|
5028
|
+
if (typeof err.responseBody === "string") {
|
|
5029
|
+
return err.responseBody.trim() ? `Details: ${err.responseBody}` : "";
|
|
5030
|
+
}
|
|
5031
|
+
if (err.responseBody && typeof err.responseBody === "object") {
|
|
5032
|
+
return `Details: ${JSON.stringify(err.responseBody, null, 2)}`;
|
|
5033
|
+
}
|
|
5034
|
+
return err.responseBody ? `Details: ${String(err.responseBody)}` : "";
|
|
5035
|
+
})();
|
|
5036
|
+
const suffix = details ? `
|
|
5037
|
+
${details}
|
|
5038
|
+
` : "\n";
|
|
5039
|
+
console.error(pc10.red(`
|
|
5040
|
+
\u2717 ${err.message}${suffix}`));
|
|
5041
|
+
process.exit(err.exitCode);
|
|
5042
|
+
}
|
|
5043
|
+
if (err instanceof DependencyError) {
|
|
5044
|
+
console.error(pc10.red(`
|
|
5045
|
+
\u2717 Missing dependency: ${err.dependency}`));
|
|
5046
|
+
console.error(pc10.yellow(`
|
|
5047
|
+
${err.message}
|
|
5048
|
+
`));
|
|
5049
|
+
console.error(pc10.dim("Run `supercheck doctor` for a full dependency check.\n"));
|
|
5050
|
+
process.exit(3 /* ConfigError */);
|
|
5051
|
+
}
|
|
4060
5052
|
if (err instanceof CLIError) {
|
|
4061
5053
|
if (err.exitCode !== 0 /* Success */) {
|
|
4062
|
-
console.error(
|
|
5054
|
+
console.error(pc10.red(`
|
|
4063
5055
|
\u2717 ${err.message}
|
|
4064
5056
|
`));
|
|
4065
5057
|
}
|
|
@@ -4068,7 +5060,7 @@ async function main() {
|
|
|
4068
5060
|
if (err && typeof err === "object" && "exitCode" in err && typeof err.exitCode === "number") {
|
|
4069
5061
|
process.exit(err.exitCode);
|
|
4070
5062
|
}
|
|
4071
|
-
console.error(
|
|
5063
|
+
console.error(pc10.red(`
|
|
4072
5064
|
\u2717 Unexpected error: ${err instanceof Error ? err.message : String(err)}
|
|
4073
5065
|
`));
|
|
4074
5066
|
process.exit(1 /* GeneralError */);
|