@supercheck/cli 0.1.0-beta.9 → 0.1.1-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -6
- package/dist/bin/supercheck.js +1294 -277
- package/dist/bin/supercheck.js.map +1 -1
- package/package.json +3 -2
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.1" : "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;
|
|
@@ -410,7 +448,7 @@ var ApiClient = class {
|
|
|
410
448
|
return this.request("DELETE", path);
|
|
411
449
|
}
|
|
412
450
|
sleep(ms) {
|
|
413
|
-
return new Promise((
|
|
451
|
+
return new Promise((resolve10) => setTimeout(resolve10, ms));
|
|
414
452
|
}
|
|
415
453
|
};
|
|
416
454
|
var defaultClient = null;
|
|
@@ -430,9 +468,9 @@ function getApiClient(options) {
|
|
|
430
468
|
import { readdirSync, statSync, readFileSync } from "fs";
|
|
431
469
|
import { resolve, relative, basename } from "path";
|
|
432
470
|
function matchGlob(filePath, pattern) {
|
|
433
|
-
const withPlaceholders = pattern.replace(/\*\*/g, "
|
|
471
|
+
const withPlaceholders = pattern.replace(/\/\*\*\//g, "__GLOBSTAR_SLASH__").replace(/\*\*/g, "__GLOBSTAR__").replace(/\*/g, "__STAR__").replace(/\?/g, "__QMARK__");
|
|
434
472
|
const escaped = withPlaceholders.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
435
|
-
const regexStr = escaped.replace(
|
|
473
|
+
const regexStr = escaped.replace(/__GLOBSTAR_SLASH__/g, "(?:/|/.+/)").replace(/__GLOBSTAR__/g, ".*").replace(/__STAR__/g, "[^/]*").replace(/__QMARK__/g, ".");
|
|
436
474
|
return new RegExp(`^${regexStr}$`).test(filePath);
|
|
437
475
|
}
|
|
438
476
|
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
@@ -526,8 +564,29 @@ function readFileContent(filePath) {
|
|
|
526
564
|
}
|
|
527
565
|
}
|
|
528
566
|
|
|
567
|
+
// src/utils/slug.ts
|
|
568
|
+
function slugify(text) {
|
|
569
|
+
return text.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 50) || "test";
|
|
570
|
+
}
|
|
571
|
+
function testFilename(testId, title, testType) {
|
|
572
|
+
const ext = testType === "k6" ? ".k6.ts" : ".pw.ts";
|
|
573
|
+
const slug = slugify(title);
|
|
574
|
+
return `${slug}.${testId}${ext}`;
|
|
575
|
+
}
|
|
576
|
+
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;
|
|
577
|
+
function extractUuidFromFilename(filename) {
|
|
578
|
+
const base = filename.split(/[/\\]/).pop() ?? filename;
|
|
579
|
+
const stem = base.replace(/\.(pw|k6)\.ts$/, "");
|
|
580
|
+
const match = UUID_EXTRACT_REGEX.exec(stem);
|
|
581
|
+
return match ? match[1] : void 0;
|
|
582
|
+
}
|
|
583
|
+
function stripTitleMetadata(script) {
|
|
584
|
+
let result = script.replace(/^\/\/\s*@title\s+[^\n]+\n\n?/, "");
|
|
585
|
+
result = result.replace(/\n\s*\*\s*@title\s+[^\n]+/, "");
|
|
586
|
+
return result;
|
|
587
|
+
}
|
|
588
|
+
|
|
529
589
|
// 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
590
|
function buildLocalResources(config, cwd) {
|
|
532
591
|
const resources = [];
|
|
533
592
|
if (config.jobs) {
|
|
@@ -536,7 +595,10 @@ function buildLocalResources(config, cwd) {
|
|
|
536
595
|
id: job.id,
|
|
537
596
|
type: "job",
|
|
538
597
|
name: job.name,
|
|
539
|
-
definition: {
|
|
598
|
+
definition: {
|
|
599
|
+
...job,
|
|
600
|
+
tests: normalizeJobTestsToIds(job.tests)
|
|
601
|
+
}
|
|
540
602
|
});
|
|
541
603
|
}
|
|
542
604
|
}
|
|
@@ -596,20 +658,24 @@ function buildLocalResources(config, cwd) {
|
|
|
596
658
|
for (const file of files) {
|
|
597
659
|
const script = readFileContent(file.absolutePath);
|
|
598
660
|
if (script) {
|
|
599
|
-
const
|
|
600
|
-
const testId = UUID_REGEX.test(stem) ? stem : void 0;
|
|
661
|
+
const testId = extractUuidFromFilename(file.filename);
|
|
601
662
|
if (!testId) {
|
|
602
663
|
logger.debug(`Test file "${file.filename}" does not have a UUID-based filename \u2014 will be treated as a new resource`);
|
|
603
664
|
}
|
|
665
|
+
const titleMatch = script.match(/@title\s+(.+)$/m);
|
|
666
|
+
const stem = file.filename.replace(/\.(pw|k6)\.ts$/, "");
|
|
667
|
+
const title = titleMatch ? titleMatch[1].trim() : stem;
|
|
668
|
+
const cleanScript = stripTitleMetadata(script);
|
|
669
|
+
const displayName = file.relativePath.replace(/^_supercheck_\//, "");
|
|
604
670
|
resources.push({
|
|
605
671
|
id: testId,
|
|
606
672
|
type: "test",
|
|
607
|
-
name:
|
|
673
|
+
name: displayName,
|
|
608
674
|
definition: {
|
|
609
675
|
id: testId,
|
|
610
|
-
title
|
|
676
|
+
title,
|
|
611
677
|
testType: file.type === "playwright" ? "playwright" : "k6",
|
|
612
|
-
script
|
|
678
|
+
script: cleanScript
|
|
613
679
|
}
|
|
614
680
|
});
|
|
615
681
|
}
|
|
@@ -699,13 +765,20 @@ function normalizeRemoteRaw(type, raw) {
|
|
|
699
765
|
if (!normalized.alertConfig) delete normalized.alertConfig;
|
|
700
766
|
}
|
|
701
767
|
}
|
|
768
|
+
if (type === "test") {
|
|
769
|
+
if (normalized.type === "browser") normalized.testType = "playwright";
|
|
770
|
+
else if (normalized.type === "performance") normalized.testType = "k6";
|
|
771
|
+
delete normalized.type;
|
|
772
|
+
if (typeof normalized.script === "string") {
|
|
773
|
+
normalized.script = stripTitleMetadata(normalized.script);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
702
776
|
if (type === "job") {
|
|
703
777
|
if (Array.isArray(normalized.tests)) {
|
|
704
778
|
normalized.tests = normalized.tests.map((t) => {
|
|
705
|
-
if (typeof t === "string") return t;
|
|
779
|
+
if (typeof t === "string") return extractUuidFromFilename(t) ?? t;
|
|
706
780
|
if (t && typeof t === "object" && t.id) {
|
|
707
|
-
|
|
708
|
-
return `_supercheck_/tests/${t.id}.${testType}.ts`;
|
|
781
|
+
return String(t.id);
|
|
709
782
|
}
|
|
710
783
|
return t;
|
|
711
784
|
});
|
|
@@ -730,6 +803,13 @@ function normalizeRemoteRaw(type, raw) {
|
|
|
730
803
|
}
|
|
731
804
|
return normalized;
|
|
732
805
|
}
|
|
806
|
+
function normalizeJobTestsToIds(tests) {
|
|
807
|
+
if (!tests) return tests;
|
|
808
|
+
return tests.map((t) => {
|
|
809
|
+
const id = extractUuidFromFilename(t);
|
|
810
|
+
return id ?? t;
|
|
811
|
+
});
|
|
812
|
+
}
|
|
733
813
|
async function fetchRemoteResources(client) {
|
|
734
814
|
const resources = [];
|
|
735
815
|
try {
|
|
@@ -746,7 +826,7 @@ async function fetchRemoteResources(client) {
|
|
|
746
826
|
logFetchError("jobs", err);
|
|
747
827
|
}
|
|
748
828
|
try {
|
|
749
|
-
const tests = await fetchAllPages(client, "/api/tests", "tests");
|
|
829
|
+
const tests = await fetchAllPages(client, "/api/tests?includeScript=true", "tests");
|
|
750
830
|
for (const test of tests) {
|
|
751
831
|
const id = safeId(test.id);
|
|
752
832
|
if (!id) {
|
|
@@ -1194,11 +1274,11 @@ var alertConfigSchema = z.object({
|
|
|
1194
1274
|
customMessage: z.string().optional()
|
|
1195
1275
|
}).passthrough();
|
|
1196
1276
|
var playwrightTestConfigSchema = z.object({
|
|
1197
|
-
testMatch: z.string().default("_supercheck_/
|
|
1277
|
+
testMatch: z.string().default("_supercheck_/playwright/**/*.pw.ts"),
|
|
1198
1278
|
browser: z.enum(["chromium", "firefox", "webkit"]).default("chromium")
|
|
1199
1279
|
});
|
|
1200
1280
|
var k6TestConfigSchema = z.object({
|
|
1201
|
-
testMatch: z.string().default("_supercheck_/
|
|
1281
|
+
testMatch: z.string().default("_supercheck_/k6/**/*.k6.ts")
|
|
1202
1282
|
});
|
|
1203
1283
|
var testsConfigSchema = z.object({
|
|
1204
1284
|
playwright: playwrightTestConfigSchema.optional(),
|
|
@@ -1372,6 +1452,8 @@ function validateNoSecrets(config) {
|
|
|
1372
1452
|
}
|
|
1373
1453
|
async function loadConfig(options = {}) {
|
|
1374
1454
|
const cwd = options.cwd ?? process.cwd();
|
|
1455
|
+
const { config: loadEnv } = await import("dotenv");
|
|
1456
|
+
loadEnv({ path: resolve2(cwd, ".env") });
|
|
1375
1457
|
const configPath = resolveConfigPath(cwd, options.configPath);
|
|
1376
1458
|
if (!configPath) {
|
|
1377
1459
|
throw new CLIError(
|
|
@@ -1481,21 +1563,21 @@ var locationsCommand = new Command2("locations").description("List available exe
|
|
|
1481
1563
|
// src/commands/config.ts
|
|
1482
1564
|
import { Command as Command3 } from "commander";
|
|
1483
1565
|
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();
|
|
1566
|
+
configCommand.command("validate").description("Validate supercheck.config.ts").option("--config <path>", "Path to config file").action(async (options) => {
|
|
1567
|
+
const { config, configPath } = await loadConfig({ configPath: options.config });
|
|
1486
1568
|
logger.success(`Valid configuration loaded from ${configPath}`);
|
|
1487
1569
|
logger.info(` Organization: ${config.project.organization}`);
|
|
1488
1570
|
logger.info(` Project: ${config.project.project}`);
|
|
1489
1571
|
});
|
|
1490
|
-
configCommand.command("print").description("Print the resolved configuration").action(async () => {
|
|
1491
|
-
const { config } = await loadConfig();
|
|
1572
|
+
configCommand.command("print").description("Print the resolved configuration").option("--config <path>", "Path to config file").action(async (options) => {
|
|
1573
|
+
const { config } = await loadConfig({ configPath: options.config });
|
|
1492
1574
|
output(config);
|
|
1493
1575
|
});
|
|
1494
1576
|
|
|
1495
1577
|
// src/commands/init.ts
|
|
1496
1578
|
import { Command as Command4 } from "commander";
|
|
1497
|
-
import { existsSync as
|
|
1498
|
-
import { resolve as
|
|
1579
|
+
import { existsSync as existsSync4, mkdirSync, writeFileSync as writeFileSync2, readFileSync as readFileSync2, appendFileSync } from "fs";
|
|
1580
|
+
import { resolve as resolve5 } from "path";
|
|
1499
1581
|
|
|
1500
1582
|
// src/utils/package-manager.ts
|
|
1501
1583
|
import { existsSync as existsSync2, writeFileSync } from "fs";
|
|
@@ -1549,9 +1631,9 @@ async function installDependencies(cwd, pm, opts = {}) {
|
|
|
1549
1631
|
await withSpinner(
|
|
1550
1632
|
`Installing ${devDeps.length} packages...`,
|
|
1551
1633
|
async () => {
|
|
1552
|
-
const { execSync } = await import("child_process");
|
|
1634
|
+
const { execSync: execSync2 } = await import("child_process");
|
|
1553
1635
|
try {
|
|
1554
|
-
|
|
1636
|
+
execSync2(cmd, {
|
|
1555
1637
|
cwd,
|
|
1556
1638
|
stdio: "pipe",
|
|
1557
1639
|
timeout: 12e4,
|
|
@@ -1573,6 +1655,200 @@ Error: ${message}`,
|
|
|
1573
1655
|
);
|
|
1574
1656
|
}
|
|
1575
1657
|
|
|
1658
|
+
// src/utils/deps.ts
|
|
1659
|
+
import { execSync } from "child_process";
|
|
1660
|
+
import { existsSync as existsSync3 } from "fs";
|
|
1661
|
+
import { resolve as resolve4 } from "path";
|
|
1662
|
+
import pc3 from "picocolors";
|
|
1663
|
+
function isCommandAvailable(command) {
|
|
1664
|
+
try {
|
|
1665
|
+
const output2 = execSync(`${command} --version`, {
|
|
1666
|
+
stdio: "pipe",
|
|
1667
|
+
timeout: 1e4,
|
|
1668
|
+
env: { ...process.env }
|
|
1669
|
+
}).toString().trim();
|
|
1670
|
+
const version = output2.split("\n")[0].trim();
|
|
1671
|
+
return { available: true, version };
|
|
1672
|
+
} catch {
|
|
1673
|
+
return { available: false };
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
function isPlaywrightPackageInstalled(cwd) {
|
|
1677
|
+
try {
|
|
1678
|
+
const pkgPath = resolve4(cwd, "node_modules", "@playwright", "test", "package.json");
|
|
1679
|
+
return existsSync3(pkgPath);
|
|
1680
|
+
} catch {
|
|
1681
|
+
return false;
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
function isK6Installed() {
|
|
1685
|
+
const result = isCommandAvailable("k6");
|
|
1686
|
+
return { installed: result.available, version: result.version };
|
|
1687
|
+
}
|
|
1688
|
+
function isNodeInstalled() {
|
|
1689
|
+
return { installed: true, version: process.version };
|
|
1690
|
+
}
|
|
1691
|
+
async function installPlaywrightBrowsers(cwd, browser = "chromium") {
|
|
1692
|
+
try {
|
|
1693
|
+
logger.info(`Installing Playwright ${browser} browser...`);
|
|
1694
|
+
execSync(`npx playwright install ${browser}`, {
|
|
1695
|
+
cwd,
|
|
1696
|
+
stdio: "inherit",
|
|
1697
|
+
timeout: 3e5,
|
|
1698
|
+
// 5 min timeout for browser download
|
|
1699
|
+
env: { ...process.env }
|
|
1700
|
+
});
|
|
1701
|
+
return true;
|
|
1702
|
+
} catch {
|
|
1703
|
+
return false;
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
function checkAllDependencies(cwd) {
|
|
1707
|
+
const deps = [];
|
|
1708
|
+
const node = isNodeInstalled();
|
|
1709
|
+
deps.push({
|
|
1710
|
+
name: "Node.js",
|
|
1711
|
+
installed: node.installed,
|
|
1712
|
+
version: node.version,
|
|
1713
|
+
required: true,
|
|
1714
|
+
installHint: "https://nodejs.org/"
|
|
1715
|
+
});
|
|
1716
|
+
const pwPkg = isPlaywrightPackageInstalled(cwd);
|
|
1717
|
+
deps.push({
|
|
1718
|
+
name: "@playwright/test",
|
|
1719
|
+
installed: pwPkg,
|
|
1720
|
+
detail: pwPkg ? "npm package found" : "npm package not found",
|
|
1721
|
+
required: true,
|
|
1722
|
+
installHint: "npm install --save-dev @playwright/test"
|
|
1723
|
+
});
|
|
1724
|
+
if (pwPkg) {
|
|
1725
|
+
const pwCheck = isCommandAvailable("npx playwright --version");
|
|
1726
|
+
deps.push({
|
|
1727
|
+
name: "Playwright browsers",
|
|
1728
|
+
installed: pwCheck.available,
|
|
1729
|
+
version: pwCheck.version,
|
|
1730
|
+
detail: pwCheck.available ? "browsers available" : "run: npx playwright install chromium",
|
|
1731
|
+
required: true,
|
|
1732
|
+
installHint: "npx playwright install chromium"
|
|
1733
|
+
});
|
|
1734
|
+
} else {
|
|
1735
|
+
deps.push({
|
|
1736
|
+
name: "Playwright browsers",
|
|
1737
|
+
installed: false,
|
|
1738
|
+
detail: "install @playwright/test first",
|
|
1739
|
+
required: true,
|
|
1740
|
+
installHint: "npm install --save-dev @playwright/test && npx playwright install chromium"
|
|
1741
|
+
});
|
|
1742
|
+
}
|
|
1743
|
+
const k6 = isK6Installed();
|
|
1744
|
+
deps.push({
|
|
1745
|
+
name: "k6",
|
|
1746
|
+
installed: k6.installed,
|
|
1747
|
+
version: k6.version,
|
|
1748
|
+
detail: k6.installed ? void 0 : "needed for performance tests",
|
|
1749
|
+
required: false,
|
|
1750
|
+
installHint: getK6InstallHint()
|
|
1751
|
+
});
|
|
1752
|
+
return deps;
|
|
1753
|
+
}
|
|
1754
|
+
function getK6InstallHint() {
|
|
1755
|
+
const platform = process.platform;
|
|
1756
|
+
switch (platform) {
|
|
1757
|
+
case "darwin":
|
|
1758
|
+
return "brew install k6";
|
|
1759
|
+
case "linux":
|
|
1760
|
+
return "sudo snap install k6 or see https://grafana.com/docs/k6/latest/set-up/install-k6/";
|
|
1761
|
+
case "win32":
|
|
1762
|
+
return "choco install k6 or winget install k6";
|
|
1763
|
+
default:
|
|
1764
|
+
return "https://grafana.com/docs/k6/latest/set-up/install-k6/";
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
function formatDependencyReport(deps) {
|
|
1768
|
+
const lines = [];
|
|
1769
|
+
let allGood = true;
|
|
1770
|
+
for (const dep of deps) {
|
|
1771
|
+
const icon = dep.installed ? pc3.green("\u2713") : dep.required ? pc3.red("\u2717") : pc3.yellow("\u25CB");
|
|
1772
|
+
const name = dep.installed ? dep.name : pc3.dim(dep.name);
|
|
1773
|
+
const version = dep.version ? pc3.dim(` (${dep.version})`) : "";
|
|
1774
|
+
const detail = dep.detail && !dep.installed ? pc3.dim(` \u2014 ${dep.detail}`) : "";
|
|
1775
|
+
lines.push(` ${icon} ${name}${version}${detail}`);
|
|
1776
|
+
if (!dep.installed && dep.required) {
|
|
1777
|
+
allGood = false;
|
|
1778
|
+
lines.push(` ${pc3.dim("Install:")} ${dep.installHint}`);
|
|
1779
|
+
} else if (!dep.installed && !dep.required) {
|
|
1780
|
+
lines.push(` ${pc3.dim("Install (optional):")} ${dep.installHint}`);
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
if (allGood) {
|
|
1784
|
+
lines.push("");
|
|
1785
|
+
lines.push(pc3.green(" All required dependencies are installed!"));
|
|
1786
|
+
}
|
|
1787
|
+
return lines.join("\n");
|
|
1788
|
+
}
|
|
1789
|
+
function ensureDependenciesForTestType(cwd, testType) {
|
|
1790
|
+
if (testType === "playwright") {
|
|
1791
|
+
if (!isPlaywrightPackageInstalled(cwd)) {
|
|
1792
|
+
throw new DependencyError(
|
|
1793
|
+
"playwright",
|
|
1794
|
+
"@playwright/test is not installed.\n Install it with: npm install --save-dev @playwright/test\n Then install browsers: npx playwright install chromium"
|
|
1795
|
+
);
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
if (testType === "k6") {
|
|
1799
|
+
const k6 = isK6Installed();
|
|
1800
|
+
if (!k6.installed) {
|
|
1801
|
+
throw new DependencyError(
|
|
1802
|
+
"k6",
|
|
1803
|
+
`k6 is not installed.
|
|
1804
|
+
Install it with: ${getK6InstallHint()}
|
|
1805
|
+
Documentation: https://grafana.com/docs/k6/latest/set-up/install-k6/`
|
|
1806
|
+
);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
var DependencyError = class extends Error {
|
|
1811
|
+
dependency;
|
|
1812
|
+
constructor(dependency, message) {
|
|
1813
|
+
super(message);
|
|
1814
|
+
this.name = "DependencyError";
|
|
1815
|
+
this.dependency = dependency;
|
|
1816
|
+
}
|
|
1817
|
+
};
|
|
1818
|
+
|
|
1819
|
+
// src/utils/uuid.ts
|
|
1820
|
+
import { randomBytes } from "crypto";
|
|
1821
|
+
function uuidv7() {
|
|
1822
|
+
const now = Date.now();
|
|
1823
|
+
const timestampBytes = new Uint8Array(6);
|
|
1824
|
+
let ts = now;
|
|
1825
|
+
for (let i = 5; i >= 0; i--) {
|
|
1826
|
+
timestampBytes[i] = ts & 255;
|
|
1827
|
+
ts = Math.floor(ts / 256);
|
|
1828
|
+
}
|
|
1829
|
+
const randBytes = randomBytes(10);
|
|
1830
|
+
const bytes = new Uint8Array(16);
|
|
1831
|
+
bytes.set(timestampBytes, 0);
|
|
1832
|
+
bytes[6] = 112 | randBytes[0] & 15;
|
|
1833
|
+
bytes[7] = randBytes[1];
|
|
1834
|
+
bytes[8] = 128 | randBytes[2] & 63;
|
|
1835
|
+
bytes[9] = randBytes[3];
|
|
1836
|
+
bytes[10] = randBytes[4];
|
|
1837
|
+
bytes[11] = randBytes[5];
|
|
1838
|
+
bytes[12] = randBytes[6];
|
|
1839
|
+
bytes[13] = randBytes[7];
|
|
1840
|
+
bytes[14] = randBytes[8];
|
|
1841
|
+
bytes[15] = randBytes[9];
|
|
1842
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
1843
|
+
return [
|
|
1844
|
+
hex.slice(0, 8),
|
|
1845
|
+
hex.slice(8, 12),
|
|
1846
|
+
hex.slice(12, 16),
|
|
1847
|
+
hex.slice(16, 20),
|
|
1848
|
+
hex.slice(20, 32)
|
|
1849
|
+
].join("-");
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1576
1852
|
// src/commands/init.ts
|
|
1577
1853
|
var CONFIG_TEMPLATE = `import { defineConfig } from '@supercheck/cli'
|
|
1578
1854
|
|
|
@@ -1587,23 +1863,27 @@ export default defineConfig({
|
|
|
1587
1863
|
},
|
|
1588
1864
|
tests: {
|
|
1589
1865
|
playwright: {
|
|
1590
|
-
testMatch: '_supercheck_/
|
|
1866
|
+
testMatch: '_supercheck_/playwright/**/*.pw.ts',
|
|
1591
1867
|
browser: 'chromium',
|
|
1592
1868
|
},
|
|
1593
1869
|
k6: {
|
|
1594
|
-
testMatch: '_supercheck_/
|
|
1870
|
+
testMatch: '_supercheck_/k6/**/*.k6.ts',
|
|
1595
1871
|
},
|
|
1596
1872
|
},
|
|
1597
1873
|
})
|
|
1598
1874
|
`;
|
|
1599
|
-
var EXAMPLE_PW_TEST =
|
|
1875
|
+
var EXAMPLE_PW_TEST = `// @title Homepage Check
|
|
1876
|
+
|
|
1877
|
+
import { test, expect } from '@playwright/test'
|
|
1600
1878
|
|
|
1601
1879
|
test('homepage loads successfully', async ({ page }) => {
|
|
1602
1880
|
await page.goto('https://example.com')
|
|
1603
1881
|
await expect(page).toHaveTitle(/Example/)
|
|
1604
1882
|
})
|
|
1605
1883
|
`;
|
|
1606
|
-
var EXAMPLE_K6_TEST =
|
|
1884
|
+
var EXAMPLE_K6_TEST = `// @title Load Test
|
|
1885
|
+
|
|
1886
|
+
import http from 'k6/http'
|
|
1607
1887
|
import { check, sleep } from 'k6'
|
|
1608
1888
|
|
|
1609
1889
|
export const options = {
|
|
@@ -1631,7 +1911,7 @@ var SUPERCHECK_TSCONFIG = `{
|
|
|
1631
1911
|
"resolveJsonModule": true,
|
|
1632
1912
|
"isolatedModules": true,
|
|
1633
1913
|
"noEmit": true,
|
|
1634
|
-
"types": ["node"]
|
|
1914
|
+
"types": ["node", "k6"]
|
|
1635
1915
|
},
|
|
1636
1916
|
"include": [
|
|
1637
1917
|
"supercheck.config.ts",
|
|
@@ -1648,8 +1928,8 @@ supercheck.config.local.mjs
|
|
|
1648
1928
|
`;
|
|
1649
1929
|
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
1930
|
const cwd = process.cwd();
|
|
1651
|
-
const configPath =
|
|
1652
|
-
if (
|
|
1931
|
+
const configPath = resolve5(cwd, "supercheck.config.ts");
|
|
1932
|
+
if (existsSync4(configPath) && !options.force) {
|
|
1653
1933
|
throw new CLIError(
|
|
1654
1934
|
"supercheck.config.ts already exists. Use --force to overwrite.",
|
|
1655
1935
|
3 /* ConfigError */
|
|
@@ -1660,43 +1940,42 @@ var initCommand = new Command4("init").description("Initialize a new Supercheck
|
|
|
1660
1940
|
logger.newline();
|
|
1661
1941
|
writeFileSync2(configPath, CONFIG_TEMPLATE, "utf-8");
|
|
1662
1942
|
logger.success("Created supercheck.config.ts");
|
|
1663
|
-
const tsconfigPath =
|
|
1664
|
-
if (!
|
|
1943
|
+
const tsconfigPath = resolve5(cwd, "tsconfig.supercheck.json");
|
|
1944
|
+
if (!existsSync4(tsconfigPath) || options.force) {
|
|
1665
1945
|
writeFileSync2(tsconfigPath, SUPERCHECK_TSCONFIG, "utf-8");
|
|
1666
1946
|
logger.success("Created tsconfig.supercheck.json (IDE IntelliSense)");
|
|
1667
1947
|
}
|
|
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
|
-
}
|
|
1948
|
+
const supercheckDir = resolve5(cwd, "_supercheck_");
|
|
1949
|
+
const playwrightDir = resolve5(supercheckDir, "playwright");
|
|
1950
|
+
const k6Dir = resolve5(supercheckDir, "k6");
|
|
1951
|
+
for (const dir of [supercheckDir, playwrightDir, k6Dir]) {
|
|
1952
|
+
if (!existsSync4(dir)) {
|
|
1953
|
+
mkdirSync(dir, { recursive: true });
|
|
1683
1954
|
}
|
|
1684
1955
|
}
|
|
1685
1956
|
logger.success("Created _supercheck_/ directory structure");
|
|
1686
1957
|
if (!options.skipExamples) {
|
|
1687
|
-
const
|
|
1688
|
-
|
|
1958
|
+
const pwTitle = "Homepage Check";
|
|
1959
|
+
const pwId = uuidv7();
|
|
1960
|
+
const pwSlug = slugify(pwTitle);
|
|
1961
|
+
const pwFilename = `${pwSlug}.${pwId}.pw.ts`;
|
|
1962
|
+
const pwTestPath = resolve5(cwd, `_supercheck_/playwright/${pwFilename}`);
|
|
1963
|
+
if (!existsSync4(pwTestPath)) {
|
|
1689
1964
|
writeFileSync2(pwTestPath, EXAMPLE_PW_TEST, "utf-8");
|
|
1690
|
-
logger.success(
|
|
1691
|
-
}
|
|
1692
|
-
const
|
|
1693
|
-
|
|
1965
|
+
logger.success(`Created _supercheck_/playwright/${pwFilename} (Playwright example)`);
|
|
1966
|
+
}
|
|
1967
|
+
const k6Title = "Load Test";
|
|
1968
|
+
const k6Id = uuidv7();
|
|
1969
|
+
const k6Slug = slugify(k6Title);
|
|
1970
|
+
const k6Filename = `${k6Slug}.${k6Id}.k6.ts`;
|
|
1971
|
+
const k6TestPath = resolve5(cwd, `_supercheck_/k6/${k6Filename}`);
|
|
1972
|
+
if (!existsSync4(k6TestPath)) {
|
|
1694
1973
|
writeFileSync2(k6TestPath, EXAMPLE_K6_TEST, "utf-8");
|
|
1695
|
-
logger.success(
|
|
1974
|
+
logger.success(`Created _supercheck_/k6/${k6Filename} (k6 example)`);
|
|
1696
1975
|
}
|
|
1697
1976
|
}
|
|
1698
|
-
const gitignorePath =
|
|
1699
|
-
if (
|
|
1977
|
+
const gitignorePath = resolve5(cwd, ".gitignore");
|
|
1978
|
+
if (existsSync4(gitignorePath)) {
|
|
1700
1979
|
const content = readFileSync2(gitignorePath, "utf-8");
|
|
1701
1980
|
if (!content.includes("supercheck.config.local")) {
|
|
1702
1981
|
appendFileSync(gitignorePath, GITIGNORE_ADDITIONS, "utf-8");
|
|
@@ -1710,16 +1989,33 @@ var initCommand = new Command4("init").description("Initialize a new Supercheck
|
|
|
1710
1989
|
logger.success("Created package.json");
|
|
1711
1990
|
}
|
|
1712
1991
|
await installDependencies(cwd, pm, {
|
|
1713
|
-
packages: options.skipExamples ? ["@supercheck/cli", "typescript", "@types/node"] : ["@supercheck/cli", "typescript", "@types/node", "@playwright/test"],
|
|
1992
|
+
packages: options.skipExamples ? ["@supercheck/cli", "typescript", "@types/node", "@types/k6"] : ["@supercheck/cli", "typescript", "@types/node", "@playwright/test", "@types/k6"],
|
|
1714
1993
|
skipInstall: options.skipInstall ?? false
|
|
1715
1994
|
});
|
|
1995
|
+
if (!options.skipInstall && !options.skipExamples) {
|
|
1996
|
+
logger.newline();
|
|
1997
|
+
const browserInstalled = await installPlaywrightBrowsers(cwd, "chromium");
|
|
1998
|
+
if (browserInstalled) {
|
|
1999
|
+
logger.success("Playwright chromium browser installed");
|
|
2000
|
+
} else {
|
|
2001
|
+
logger.warn("Could not install Playwright browsers automatically.");
|
|
2002
|
+
logger.info(" Run manually: npx playwright install chromium");
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
const k6Status = isK6Installed();
|
|
1716
2006
|
logger.newline();
|
|
1717
2007
|
logger.header("Supercheck project initialized!");
|
|
1718
2008
|
logger.newline();
|
|
2009
|
+
if (!k6Status.installed) {
|
|
2010
|
+
logger.warn("k6 is not installed (needed for performance/load tests)");
|
|
2011
|
+
logger.info(` Install: ${getK6InstallHint()}`);
|
|
2012
|
+
logger.info(" Docs: https://grafana.com/docs/k6/latest/set-up/install-k6/");
|
|
2013
|
+
logger.newline();
|
|
2014
|
+
}
|
|
1719
2015
|
logger.info("Next steps:");
|
|
1720
2016
|
logger.info(" 1. Edit supercheck.config.ts with your org/project details");
|
|
1721
2017
|
logger.info(" 2. Run `supercheck login --token <your-token>` to authenticate");
|
|
1722
|
-
logger.info(" 3. Write tests in _supercheck_/
|
|
2018
|
+
logger.info(" 3. Write tests in _supercheck_/playwright and _supercheck_/k6");
|
|
1723
2019
|
logger.info(" 4. Run `supercheck diff` to preview changes against the cloud");
|
|
1724
2020
|
logger.info(" 5. Run `supercheck deploy` to push to Supercheck");
|
|
1725
2021
|
logger.info(" 6. Run `supercheck pull` to sync cloud resources locally");
|
|
@@ -1734,6 +2030,8 @@ var initCommand = new Command4("init").description("Initialize a new Supercheck
|
|
|
1734
2030
|
|
|
1735
2031
|
// src/commands/jobs.ts
|
|
1736
2032
|
import { Command as Command5 } from "commander";
|
|
2033
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
2034
|
+
import { resolve as resolve6 } from "path";
|
|
1737
2035
|
|
|
1738
2036
|
// src/api/authenticated-client.ts
|
|
1739
2037
|
function createAuthenticatedClient() {
|
|
@@ -1742,7 +2040,7 @@ function createAuthenticatedClient() {
|
|
|
1742
2040
|
return getApiClient({ token, baseUrl: baseUrl ?? void 0 });
|
|
1743
2041
|
}
|
|
1744
2042
|
|
|
1745
|
-
// src/utils/
|
|
2043
|
+
// src/utils/number.ts
|
|
1746
2044
|
function parseIntStrict(value, name, opts) {
|
|
1747
2045
|
const parsed = parseInt(value, 10);
|
|
1748
2046
|
if (!Number.isFinite(parsed)) {
|
|
@@ -1766,8 +2064,188 @@ function parseIntStrict(value, name, opts) {
|
|
|
1766
2064
|
return parsed;
|
|
1767
2065
|
}
|
|
1768
2066
|
|
|
2067
|
+
// src/utils/validation.ts
|
|
2068
|
+
function normalizeTestTypeForApi(localType) {
|
|
2069
|
+
if (typeof localType !== "string") return void 0;
|
|
2070
|
+
const normalized = localType.trim().toLowerCase();
|
|
2071
|
+
if (normalized === "playwright") return "browser";
|
|
2072
|
+
if (normalized === "k6") return "performance";
|
|
2073
|
+
if (normalized === "load") return "performance";
|
|
2074
|
+
if (normalized === "browser" || normalized === "performance" || normalized === "api" || normalized === "database" || normalized === "custom") {
|
|
2075
|
+
return normalized;
|
|
2076
|
+
}
|
|
2077
|
+
return void 0;
|
|
2078
|
+
}
|
|
2079
|
+
function formatValidationError(responseBody) {
|
|
2080
|
+
if (responseBody && typeof responseBody === "object") {
|
|
2081
|
+
const body = responseBody;
|
|
2082
|
+
if (typeof body.error === "string") {
|
|
2083
|
+
const line = typeof body.line === "number" ? body.line : void 0;
|
|
2084
|
+
const column = typeof body.column === "number" ? body.column : void 0;
|
|
2085
|
+
if (line !== void 0 && column !== void 0) {
|
|
2086
|
+
return `${body.error} (line ${line}, column ${column})`;
|
|
2087
|
+
}
|
|
2088
|
+
return body.error;
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
return "Validation failed";
|
|
2092
|
+
}
|
|
2093
|
+
async function validateScripts(client, inputs) {
|
|
2094
|
+
const results = [];
|
|
2095
|
+
for (const input of inputs) {
|
|
2096
|
+
if (!input.script || input.script.trim().length === 0) {
|
|
2097
|
+
throw new CLIError(`Test "${input.name}" has no script content`, 3 /* ConfigError */);
|
|
2098
|
+
}
|
|
2099
|
+
try {
|
|
2100
|
+
const { data } = await client.post(
|
|
2101
|
+
"/api/validate-script",
|
|
2102
|
+
{
|
|
2103
|
+
script: input.script,
|
|
2104
|
+
testType: input.testType
|
|
2105
|
+
}
|
|
2106
|
+
);
|
|
2107
|
+
results.push({
|
|
2108
|
+
name: input.name,
|
|
2109
|
+
valid: data?.valid ?? true,
|
|
2110
|
+
warnings: data?.warnings
|
|
2111
|
+
});
|
|
2112
|
+
} catch (err) {
|
|
2113
|
+
if (err instanceof ApiRequestError) {
|
|
2114
|
+
results.push({
|
|
2115
|
+
name: input.name,
|
|
2116
|
+
valid: false,
|
|
2117
|
+
error: formatValidationError(err.responseBody)
|
|
2118
|
+
});
|
|
2119
|
+
continue;
|
|
2120
|
+
}
|
|
2121
|
+
throw err;
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
return results;
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
// src/utils/exec.ts
|
|
2128
|
+
import { spawn } from "child_process";
|
|
2129
|
+
function runCommand(command, args, cwd) {
|
|
2130
|
+
return new Promise((resolve10, reject) => {
|
|
2131
|
+
const child = spawn(command, args, {
|
|
2132
|
+
stdio: "inherit",
|
|
2133
|
+
shell: true,
|
|
2134
|
+
cwd
|
|
2135
|
+
});
|
|
2136
|
+
child.on("error", (err) => reject(err));
|
|
2137
|
+
child.on("close", (code) => resolve10(code ?? 0));
|
|
2138
|
+
});
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
// src/utils/playwright.ts
|
|
2142
|
+
import { mkdtempSync, writeFileSync as writeFileSync3, rmSync } from "fs";
|
|
2143
|
+
import { join } from "path";
|
|
2144
|
+
function createTempPlaywrightConfig(cwd, testMatch) {
|
|
2145
|
+
const dir = mkdtempSync(join(cwd, ".supercheck-playwright-"));
|
|
2146
|
+
const filePath = join(dir, "playwright.supercheck.config.mjs");
|
|
2147
|
+
const match = testMatch ?? "_supercheck_/playwright/**/*.pw.ts";
|
|
2148
|
+
const contents = [
|
|
2149
|
+
"import { defineConfig } from '@playwright/test'",
|
|
2150
|
+
"",
|
|
2151
|
+
"export default defineConfig({",
|
|
2152
|
+
` testDir: ${JSON.stringify(cwd)},`,
|
|
2153
|
+
` testMatch: ${JSON.stringify([match])},`,
|
|
2154
|
+
"})",
|
|
2155
|
+
""
|
|
2156
|
+
].join("\n");
|
|
2157
|
+
writeFileSync3(filePath, contents, "utf-8");
|
|
2158
|
+
return {
|
|
2159
|
+
path: filePath,
|
|
2160
|
+
cleanup: () => {
|
|
2161
|
+
rmSync(dir, { recursive: true, force: true });
|
|
2162
|
+
}
|
|
2163
|
+
};
|
|
2164
|
+
}
|
|
2165
|
+
|
|
1769
2166
|
// src/commands/jobs.ts
|
|
1770
2167
|
var jobCommand = new Command5("job").description("Manage jobs");
|
|
2168
|
+
function inferLocalTestType(filePath) {
|
|
2169
|
+
return filePath.endsWith(".k6.ts") || filePath.endsWith(".k6.js") ? "k6" : "playwright";
|
|
2170
|
+
}
|
|
2171
|
+
function toDisplayName(cwd, filePath) {
|
|
2172
|
+
const normalized = filePath.replace(cwd, "").replace(/^\//, "");
|
|
2173
|
+
return normalized.startsWith("_supercheck_/") ? normalized.replace(/^_supercheck_\//, "") : normalized;
|
|
2174
|
+
}
|
|
2175
|
+
function resolveJobLocalTests(cwd, tests, patterns) {
|
|
2176
|
+
const files = discoverFiles(cwd, patterns);
|
|
2177
|
+
const uuidMap = /* @__PURE__ */ new Map();
|
|
2178
|
+
for (const file of files) {
|
|
2179
|
+
const uuid = extractUuidFromFilename(file.absolutePath);
|
|
2180
|
+
if (uuid) uuidMap.set(uuid, file.absolutePath);
|
|
2181
|
+
}
|
|
2182
|
+
return tests.map((ref) => {
|
|
2183
|
+
let resolved;
|
|
2184
|
+
if (ref.endsWith(".pw.ts") || ref.endsWith(".k6.ts") || ref.endsWith(".pw.js") || ref.endsWith(".k6.js")) {
|
|
2185
|
+
resolved = resolve6(cwd, ref);
|
|
2186
|
+
} else if (ref.startsWith("_supercheck_/")) {
|
|
2187
|
+
resolved = resolve6(cwd, ref);
|
|
2188
|
+
} else if (uuidMap.has(ref)) {
|
|
2189
|
+
resolved = uuidMap.get(ref);
|
|
2190
|
+
}
|
|
2191
|
+
if (!resolved) {
|
|
2192
|
+
throw new CLIError(`Cannot resolve local test for job entry: ${ref}`, 3 /* ConfigError */);
|
|
2193
|
+
}
|
|
2194
|
+
const script = readFileSync3(resolved, "utf-8");
|
|
2195
|
+
const type = inferLocalTestType(resolved);
|
|
2196
|
+
return { path: resolved, type, name: toDisplayName(cwd, resolved), script };
|
|
2197
|
+
});
|
|
2198
|
+
}
|
|
2199
|
+
async function validateJobTests(client, tests) {
|
|
2200
|
+
const inputs = tests.map((test) => ({
|
|
2201
|
+
name: test.name,
|
|
2202
|
+
script: test.script,
|
|
2203
|
+
testType: normalizeTestTypeForApi(test.type)
|
|
2204
|
+
}));
|
|
2205
|
+
const results = await validateScripts(client, inputs);
|
|
2206
|
+
const failures = results.filter((r) => !r.valid);
|
|
2207
|
+
if (failures.length > 0) {
|
|
2208
|
+
const details = failures.map((f) => ` - ${f.name}: ${f.error ?? "Validation failed"}`).join("\n");
|
|
2209
|
+
throw new CLIError(`Validation failed:
|
|
2210
|
+
${details}`, 3 /* ConfigError */);
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
async function runLocalJobTests(tests, cwd, testMatch) {
|
|
2214
|
+
const playwrightTests = tests.filter((t) => t.type === "playwright").map((t) => t.path);
|
|
2215
|
+
const k6Tests = tests.filter((t) => t.type === "k6").map((t) => t.path);
|
|
2216
|
+
try {
|
|
2217
|
+
if (playwrightTests.length > 0) {
|
|
2218
|
+
ensureDependenciesForTestType(cwd, "playwright");
|
|
2219
|
+
}
|
|
2220
|
+
if (k6Tests.length > 0) {
|
|
2221
|
+
ensureDependenciesForTestType(cwd, "k6");
|
|
2222
|
+
}
|
|
2223
|
+
} catch (err) {
|
|
2224
|
+
if (err instanceof DependencyError) {
|
|
2225
|
+
throw new CLIError(err.message, 1 /* GeneralError */);
|
|
2226
|
+
}
|
|
2227
|
+
throw err;
|
|
2228
|
+
}
|
|
2229
|
+
if (playwrightTests.length > 0) {
|
|
2230
|
+
logger.header("Running Playwright tests");
|
|
2231
|
+
const tempConfig = createTempPlaywrightConfig(cwd, testMatch);
|
|
2232
|
+
try {
|
|
2233
|
+
const code = await runCommand("npx", ["playwright", "test", "--config", tempConfig.path, ...playwrightTests], cwd);
|
|
2234
|
+
if (code !== 0) {
|
|
2235
|
+
throw new CLIError(`Playwright tests failed with exit code ${code}`, 1 /* GeneralError */);
|
|
2236
|
+
}
|
|
2237
|
+
} finally {
|
|
2238
|
+
tempConfig.cleanup();
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
for (const path of k6Tests) {
|
|
2242
|
+
logger.header(`Running k6 test: ${toDisplayName(cwd, path)}`);
|
|
2243
|
+
const code = await runCommand("k6", ["run", path], cwd);
|
|
2244
|
+
if (code !== 0) {
|
|
2245
|
+
throw new CLIError(`k6 test failed with exit code ${code}`, 1 /* GeneralError */);
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
1771
2249
|
jobCommand.command("list").description("List all jobs").option("--page <page>", "Page number", "1").option("--limit <limit>", "Items per page", "50").action(async (options) => {
|
|
1772
2250
|
const client = createAuthenticatedClient();
|
|
1773
2251
|
const { data } = await withSpinner(
|
|
@@ -1846,15 +2324,19 @@ jobCommand.command("get <id>").description("Get job details").action(async (id)
|
|
|
1846
2324
|
);
|
|
1847
2325
|
outputDetail(data);
|
|
1848
2326
|
});
|
|
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) => {
|
|
2327
|
+
jobCommand.command("create").description("Create a new job").requiredOption("--name <name>", "Job name").requiredOption("--tests <tests...>", "Test IDs (space or comma separated)").option("--description <description>", "Job description", "").option("--type <type>", "Job runner type (playwright, k6)").option("--schedule <cron>", "Cron schedule expression").option("--timeout <seconds>", "Timeout in seconds", "300").option("--retries <count>", "Retry count on failure", "0").action(async (options) => {
|
|
1850
2328
|
const client = createAuthenticatedClient();
|
|
2329
|
+
const tests = (options.tests ?? []).flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean);
|
|
2330
|
+
if (tests.length === 0) {
|
|
2331
|
+
throw new CLIError("At least one test ID is required to create a job.", 3 /* ConfigError */);
|
|
2332
|
+
}
|
|
1851
2333
|
const body = {
|
|
1852
2334
|
name: options.name,
|
|
1853
2335
|
description: options.description,
|
|
1854
2336
|
timeoutSeconds: parseIntStrict(options.timeout, "--timeout", { min: 1 }),
|
|
1855
2337
|
retryCount: parseIntStrict(options.retries, "--retries", { min: 0 }),
|
|
1856
2338
|
config: {},
|
|
1857
|
-
tests:
|
|
2339
|
+
tests: tests.map((id) => ({ id }))
|
|
1858
2340
|
};
|
|
1859
2341
|
if (options.type) body.jobType = options.type;
|
|
1860
2342
|
if (options.schedule) body.cronSchedule = options.schedule;
|
|
@@ -1862,8 +2344,10 @@ jobCommand.command("create").description("Create a new job").requiredOption("--n
|
|
|
1862
2344
|
"Creating job",
|
|
1863
2345
|
() => client.post("/api/jobs", body)
|
|
1864
2346
|
);
|
|
1865
|
-
|
|
1866
|
-
|
|
2347
|
+
const job = data.job ?? data;
|
|
2348
|
+
const jobId = job?.id;
|
|
2349
|
+
logger.success(`Job "${options.name}" created (${jobId ?? "unknown"})`);
|
|
2350
|
+
outputDetail(job);
|
|
1867
2351
|
});
|
|
1868
2352
|
jobCommand.command("update <id>").description("Update job configuration").option("--name <name>", "Job name").option("--description <description>", "Job description").option("--schedule <cron>", "Cron schedule expression").option("--timeout <seconds>", "Timeout in seconds").option("--retries <count>", "Retry count on failure").option("--status <status>", "Job status (active, paused)").action(async (id, options) => {
|
|
1869
2353
|
const client = createAuthenticatedClient();
|
|
@@ -1901,7 +2385,34 @@ jobCommand.command("delete <id>").description("Delete a job").option("--force",
|
|
|
1901
2385
|
);
|
|
1902
2386
|
logger.success(`Job ${id} deleted`);
|
|
1903
2387
|
});
|
|
1904
|
-
jobCommand.command("run").description("Run a job immediately").requiredOption("--id <id>", "Job ID to run").action(async (options) => {
|
|
2388
|
+
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) => {
|
|
2389
|
+
if (options.local && options.cloud) {
|
|
2390
|
+
throw new CLIError("--local and --cloud are mutually exclusive.", 3 /* ConfigError */);
|
|
2391
|
+
}
|
|
2392
|
+
const useLocal = options.local === true;
|
|
2393
|
+
if (useLocal) {
|
|
2394
|
+
const cwd = process.cwd();
|
|
2395
|
+
const { config } = await loadConfig({ cwd });
|
|
2396
|
+
const job = config.jobs?.find((j) => j.id === options.id || j.name === options.id);
|
|
2397
|
+
if (!job) {
|
|
2398
|
+
throw new CLIError(`Job ${options.id} not found in local config.`, 3 /* ConfigError */);
|
|
2399
|
+
}
|
|
2400
|
+
const tests2 = Array.isArray(job.tests) ? job.tests : [];
|
|
2401
|
+
if (tests2.length === 0) {
|
|
2402
|
+
throw new CLIError(`Job ${options.id} has no tests.`, 1 /* GeneralError */);
|
|
2403
|
+
}
|
|
2404
|
+
const patterns = {
|
|
2405
|
+
playwright: config.tests?.playwright?.testMatch,
|
|
2406
|
+
k6: config.tests?.k6?.testMatch
|
|
2407
|
+
};
|
|
2408
|
+
const localTests = resolveJobLocalTests(cwd, tests2, patterns);
|
|
2409
|
+
const client2 = createAuthenticatedClient();
|
|
2410
|
+
await withSpinner("Validating test scripts...", async () => {
|
|
2411
|
+
await validateJobTests(client2, localTests);
|
|
2412
|
+
}, { successText: "Test scripts validated" });
|
|
2413
|
+
await runLocalJobTests(localTests, cwd, config.tests?.playwright?.testMatch);
|
|
2414
|
+
return;
|
|
2415
|
+
}
|
|
1905
2416
|
const client = createAuthenticatedClient();
|
|
1906
2417
|
const { data: jobData } = await withSpinner(
|
|
1907
2418
|
"Fetching job details",
|
|
@@ -1941,7 +2452,11 @@ jobCommand.command("trigger <id>").description("Trigger a job run").option("--wa
|
|
|
1941
2452
|
`/api/jobs/${id}/trigger`
|
|
1942
2453
|
)
|
|
1943
2454
|
);
|
|
1944
|
-
|
|
2455
|
+
const runId = data.runId ?? data.data?.runId;
|
|
2456
|
+
if (!runId) {
|
|
2457
|
+
throw new CLIError("Job trigger response did not include a run ID.", 4 /* ApiError */);
|
|
2458
|
+
}
|
|
2459
|
+
logger.success(`Job triggered. Run ID: ${runId}`);
|
|
1945
2460
|
if (options.wait) {
|
|
1946
2461
|
let statusClient;
|
|
1947
2462
|
try {
|
|
@@ -1956,20 +2471,20 @@ jobCommand.command("trigger <id>").description("Trigger a job run").option("--wa
|
|
|
1956
2471
|
const timeoutMs = parseIntStrict(options.timeout, "--timeout", { min: 1 }) * 1e3;
|
|
1957
2472
|
const startTime = Date.now();
|
|
1958
2473
|
while (Date.now() - startTime < timeoutMs) {
|
|
1959
|
-
await new Promise((
|
|
2474
|
+
await new Promise((resolve10) => setTimeout(resolve10, 3e3));
|
|
1960
2475
|
const { data: runData } = await statusClient.get(
|
|
1961
|
-
`/api/runs/${
|
|
2476
|
+
`/api/runs/${runId}`
|
|
1962
2477
|
);
|
|
1963
2478
|
const status = typeof runData.status === "string" ? runData.status.toLowerCase() : "";
|
|
1964
2479
|
if (["passed", "failed", "error", "blocked"].includes(status)) {
|
|
1965
2480
|
if (status === "passed") {
|
|
1966
|
-
logger.success(`Run ${
|
|
2481
|
+
logger.success(`Run ${runId} passed`);
|
|
1967
2482
|
} else if (status === "blocked") {
|
|
1968
|
-
logger.error(`Run ${
|
|
1969
|
-
throw new CLIError(`Run ${
|
|
2483
|
+
logger.error(`Run ${runId} blocked`);
|
|
2484
|
+
throw new CLIError(`Run ${runId} blocked`, 1 /* GeneralError */);
|
|
1970
2485
|
} else {
|
|
1971
|
-
logger.error(`Run ${
|
|
1972
|
-
throw new CLIError(`Run ${
|
|
2486
|
+
logger.error(`Run ${runId} ${status}`);
|
|
2487
|
+
throw new CLIError(`Run ${runId} ${status}`, 1 /* GeneralError */);
|
|
1973
2488
|
}
|
|
1974
2489
|
outputDetail(runData);
|
|
1975
2490
|
return;
|
|
@@ -1985,55 +2500,14 @@ jobCommand.command("trigger <id>").description("Trigger a job run").option("--wa
|
|
|
1985
2500
|
|
|
1986
2501
|
// src/commands/runs.ts
|
|
1987
2502
|
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) => {
|
|
2503
|
+
var runCommand2 = new Command6("run").description("Manage runs");
|
|
2504
|
+
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
2505
|
const client = createAuthenticatedClient();
|
|
2033
2506
|
const params = {
|
|
2034
2507
|
page: options.page,
|
|
2035
2508
|
limit: options.limit
|
|
2036
2509
|
};
|
|
2510
|
+
if (options.job) params.jobId = options.job;
|
|
2037
2511
|
if (options.status) params.status = options.status;
|
|
2038
2512
|
const { data } = await withSpinner(
|
|
2039
2513
|
"Fetching runs",
|
|
@@ -2058,7 +2532,7 @@ Page ${data.pagination.page}/${data.pagination.totalPages} (${data.pagination.to
|
|
|
2058
2532
|
);
|
|
2059
2533
|
}
|
|
2060
2534
|
});
|
|
2061
|
-
|
|
2535
|
+
runCommand2.command("get <id>").description("Get run details").action(async (id) => {
|
|
2062
2536
|
const client = createAuthenticatedClient();
|
|
2063
2537
|
const { data } = await withSpinner(
|
|
2064
2538
|
"Fetching run details",
|
|
@@ -2066,7 +2540,7 @@ runCommand.command("get <id>").description("Get run details").action(async (id)
|
|
|
2066
2540
|
);
|
|
2067
2541
|
outputDetail(data);
|
|
2068
2542
|
});
|
|
2069
|
-
|
|
2543
|
+
runCommand2.command("status <id>").description("Get run status").action(async (id) => {
|
|
2070
2544
|
const client = createAuthenticatedClient();
|
|
2071
2545
|
const { data } = await withSpinner(
|
|
2072
2546
|
"Fetching run status",
|
|
@@ -2074,7 +2548,16 @@ runCommand.command("status <id>").description("Get run status").action(async (id
|
|
|
2074
2548
|
);
|
|
2075
2549
|
outputDetail(data);
|
|
2076
2550
|
});
|
|
2077
|
-
|
|
2551
|
+
runCommand2.command("permissions <id>").description("Get run access permissions").action(async (id) => {
|
|
2552
|
+
const client = createAuthenticatedClient();
|
|
2553
|
+
const { data } = await withSpinner(
|
|
2554
|
+
"Fetching run permissions",
|
|
2555
|
+
() => client.get(`/api/runs/${id}/permissions`)
|
|
2556
|
+
);
|
|
2557
|
+
const payload = data.data ?? data;
|
|
2558
|
+
outputDetail(payload);
|
|
2559
|
+
});
|
|
2560
|
+
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
2561
|
const token = requireAuth();
|
|
2079
2562
|
const baseUrl = getStoredBaseUrl() ?? "https://app.supercheck.io";
|
|
2080
2563
|
const idleTimeoutMs = Math.max(Number(options.idleTimeout) || 60, 10) * 1e3;
|
|
@@ -2092,7 +2575,7 @@ runCommand.command("stream <id>").description("Stream live console output from a
|
|
|
2092
2575
|
"Accept": "text/event-stream",
|
|
2093
2576
|
"User-Agent": `supercheck-cli/${CLI_VERSION}`
|
|
2094
2577
|
},
|
|
2095
|
-
...proxy ? { dispatcher:
|
|
2578
|
+
...proxy ? { dispatcher: getProxyAgent(proxy) } : {},
|
|
2096
2579
|
signal: controller.signal
|
|
2097
2580
|
});
|
|
2098
2581
|
clearTimeout(connectTimeout);
|
|
@@ -2182,62 +2665,101 @@ runCommand.command("stream <id>").description("Stream live console output from a
|
|
|
2182
2665
|
);
|
|
2183
2666
|
}
|
|
2184
2667
|
});
|
|
2668
|
+
runCommand2.command("cancel <id>").description("Cancel a running execution").action(async (id) => {
|
|
2669
|
+
const client = createAuthenticatedClient();
|
|
2670
|
+
const { data } = await withSpinner(
|
|
2671
|
+
"Cancelling run",
|
|
2672
|
+
() => client.post(`/api/runs/${id}/cancel`)
|
|
2673
|
+
);
|
|
2674
|
+
outputDetail(data);
|
|
2675
|
+
});
|
|
2185
2676
|
|
|
2186
2677
|
// src/commands/tests.ts
|
|
2187
2678
|
import { Command as Command7 } from "commander";
|
|
2188
|
-
import {
|
|
2679
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
2680
|
+
import { resolve as resolve7 } from "path";
|
|
2189
2681
|
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;
|
|
2682
|
+
function normalizeTestType(input) {
|
|
2683
|
+
return normalizeTestTypeForApi(input) ?? input.trim().toLowerCase();
|
|
2197
2684
|
}
|
|
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;
|
|
2685
|
+
function inferTestType(filename) {
|
|
2686
|
+
if (filename.endsWith(".k6.ts") || filename.endsWith(".k6.js")) return "k6";
|
|
2687
|
+
if (filename.endsWith(".pw.ts") || filename.endsWith(".pw.js") || filename.endsWith(".spec.ts")) return "playwright";
|
|
2688
|
+
return void 0;
|
|
2217
2689
|
}
|
|
2218
|
-
function
|
|
2219
|
-
const
|
|
2220
|
-
|
|
2221
|
-
|
|
2690
|
+
function toDisplayName2(cwd, filePath) {
|
|
2691
|
+
const normalized = filePath.replace(cwd, "").replace(/^\//, "");
|
|
2692
|
+
return normalized.startsWith("_supercheck_/") ? normalized.replace(/^_supercheck_\//, "") : normalized;
|
|
2693
|
+
}
|
|
2694
|
+
function collectLocalTests(cwd, patterns, options) {
|
|
2695
|
+
if (options.file) {
|
|
2696
|
+
const filePath = resolve7(cwd, options.file);
|
|
2697
|
+
const type = inferTestType(filePath) ?? (options.type === "k6" ? "k6" : "playwright");
|
|
2698
|
+
const validationType = options.type ? normalizeTestTypeForApi(options.type) : normalizeTestTypeForApi(type);
|
|
2699
|
+
const script = readFileSync4(filePath, "utf-8");
|
|
2700
|
+
return [{
|
|
2701
|
+
path: filePath,
|
|
2702
|
+
type,
|
|
2703
|
+
name: toDisplayName2(cwd, filePath),
|
|
2704
|
+
script,
|
|
2705
|
+
validationType
|
|
2706
|
+
}];
|
|
2707
|
+
}
|
|
2708
|
+
const typeFilter = options.type?.toLowerCase();
|
|
2709
|
+
const filterType = typeFilter && ["api", "database", "custom", "browser", "playwright"].includes(typeFilter) ? "playwright" : typeFilter === "performance" || typeFilter === "load" ? "k6" : typeFilter;
|
|
2710
|
+
const files = discoverFiles(cwd, patterns).filter((file) => !filterType || file.type === filterType);
|
|
2711
|
+
return files.map((file) => {
|
|
2712
|
+
const script = readFileSync4(file.absolutePath, "utf-8");
|
|
2713
|
+
const validationType = options.type ? normalizeTestTypeForApi(options.type) : normalizeTestTypeForApi(file.type);
|
|
2714
|
+
return {
|
|
2715
|
+
path: file.absolutePath,
|
|
2716
|
+
type: file.type,
|
|
2717
|
+
name: toDisplayName2(cwd, file.absolutePath),
|
|
2718
|
+
script,
|
|
2719
|
+
validationType
|
|
2720
|
+
};
|
|
2721
|
+
});
|
|
2722
|
+
}
|
|
2723
|
+
async function validateLocalTests(client, tests) {
|
|
2724
|
+
const inputs = tests.map((test) => ({
|
|
2725
|
+
name: test.name,
|
|
2726
|
+
script: test.script,
|
|
2727
|
+
testType: test.validationType ?? normalizeTestTypeForApi(test.type)
|
|
2728
|
+
}));
|
|
2729
|
+
const results = await validateScripts(client, inputs);
|
|
2730
|
+
const failures = results.filter((r) => !r.valid);
|
|
2731
|
+
if (failures.length > 0) {
|
|
2732
|
+
const details = failures.map((f) => ` - ${f.name}: ${f.error ?? "Validation failed"}`).join("\n");
|
|
2733
|
+
throw new CLIError(`Validation failed:
|
|
2734
|
+
${details}`, 3 /* ConfigError */);
|
|
2222
2735
|
}
|
|
2223
|
-
|
|
2224
|
-
|
|
2736
|
+
const warnings = results.filter((r) => r.warnings && r.warnings.length > 0);
|
|
2737
|
+
for (const warn of warnings) {
|
|
2738
|
+
logger.warn(`Validation warnings for ${warn.name}: ${(warn.warnings ?? []).join(", ")}`);
|
|
2225
2739
|
}
|
|
2226
|
-
|
|
2227
|
-
|
|
2740
|
+
}
|
|
2741
|
+
async function runPlaywrightTests(paths, cwd, testMatch) {
|
|
2742
|
+
if (paths.length === 0) return;
|
|
2743
|
+
const tempConfig = createTempPlaywrightConfig(cwd, testMatch);
|
|
2744
|
+
try {
|
|
2745
|
+
const code = await runCommand("npx", ["playwright", "test", "--config", tempConfig.path, ...paths], cwd);
|
|
2746
|
+
if (code !== 0) {
|
|
2747
|
+
throw new CLIError(`Playwright tests failed with exit code ${code}`, 1 /* GeneralError */);
|
|
2748
|
+
}
|
|
2749
|
+
} finally {
|
|
2750
|
+
tempConfig.cleanup();
|
|
2228
2751
|
}
|
|
2229
|
-
return null;
|
|
2230
2752
|
}
|
|
2231
|
-
function
|
|
2232
|
-
const
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2753
|
+
async function runK6Tests(paths, cwd) {
|
|
2754
|
+
for (const path of paths) {
|
|
2755
|
+
const code = await runCommand("k6", ["run", path], cwd);
|
|
2756
|
+
if (code !== 0) {
|
|
2757
|
+
throw new CLIError(`k6 test failed with exit code ${code}`, 1 /* GeneralError */);
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2238
2760
|
}
|
|
2239
2761
|
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 (
|
|
2762
|
+
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
2763
|
const client = createAuthenticatedClient();
|
|
2242
2764
|
const params = {
|
|
2243
2765
|
page: options.page,
|
|
@@ -2269,30 +2791,32 @@ Page ${data.pagination.page}/${data.pagination.totalPages} (${data.pagination.to
|
|
|
2269
2791
|
});
|
|
2270
2792
|
testCommand.command("get <id>").description("Get test details").option("--include-script", "Include the test script content").action(async (id, options) => {
|
|
2271
2793
|
const client = createAuthenticatedClient();
|
|
2272
|
-
const params = {
|
|
2273
|
-
|
|
2794
|
+
const params = {
|
|
2795
|
+
includeScript: options.includeScript ? "true" : "false"
|
|
2796
|
+
};
|
|
2274
2797
|
const { data } = await withSpinner(
|
|
2275
2798
|
"Fetching test details",
|
|
2276
2799
|
() => client.get(`/api/tests/${id}`, params)
|
|
2277
2800
|
);
|
|
2278
2801
|
outputDetail(data);
|
|
2279
2802
|
});
|
|
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 =
|
|
2803
|
+
testCommand.command("create").description("Create a new test").requiredOption("--title <title>", "Test title").requiredOption("--file <path>", "Path to the test script file").option("--type <type>", "Test type (browser, performance, api, database, custom)").option("--description <description>", "Test description").action(async (options) => {
|
|
2804
|
+
const { readFileSync: readFileSync6 } = await import("fs");
|
|
2805
|
+
const { resolve: resolve10 } = await import("path");
|
|
2806
|
+
const filePath = resolve10(process.cwd(), options.file);
|
|
2284
2807
|
let script;
|
|
2285
2808
|
try {
|
|
2286
|
-
script =
|
|
2809
|
+
script = readFileSync6(filePath, "utf-8");
|
|
2287
2810
|
} catch {
|
|
2288
2811
|
throw new CLIError(`Cannot read file: ${filePath}`, 1 /* GeneralError */);
|
|
2289
2812
|
}
|
|
2813
|
+
const typeArg = options.type || inferTestType(options.file) || "playwright";
|
|
2290
2814
|
const client = createAuthenticatedClient();
|
|
2291
2815
|
const encodedScript = Buffer2.from(script, "utf-8").toString("base64");
|
|
2292
2816
|
const body = {
|
|
2293
2817
|
title: options.title,
|
|
2294
2818
|
script: encodedScript,
|
|
2295
|
-
type: normalizeTestType(
|
|
2819
|
+
type: normalizeTestType(typeArg)
|
|
2296
2820
|
};
|
|
2297
2821
|
if (options.description) body.description = options.description;
|
|
2298
2822
|
const { data } = await withSpinner(
|
|
@@ -2309,11 +2833,11 @@ testCommand.command("update <id>").description("Update a test").option("--title
|
|
|
2309
2833
|
if (options.title !== void 0) body.title = options.title;
|
|
2310
2834
|
if (options.description !== void 0) body.description = options.description;
|
|
2311
2835
|
if (options.file) {
|
|
2312
|
-
const { readFileSync:
|
|
2313
|
-
const { resolve:
|
|
2314
|
-
const filePath =
|
|
2836
|
+
const { readFileSync: readFileSync6 } = await import("fs");
|
|
2837
|
+
const { resolve: resolve10 } = await import("path");
|
|
2838
|
+
const filePath = resolve10(process.cwd(), options.file);
|
|
2315
2839
|
try {
|
|
2316
|
-
const raw =
|
|
2840
|
+
const raw = readFileSync6(filePath, "utf-8");
|
|
2317
2841
|
body.script = Buffer2.from(raw, "utf-8").toString("base64");
|
|
2318
2842
|
} catch {
|
|
2319
2843
|
throw new CLIError(`Cannot read file: ${filePath}`, 1 /* GeneralError */);
|
|
@@ -2346,6 +2870,71 @@ testCommand.command("delete <id>").description("Delete a test").option("--force"
|
|
|
2346
2870
|
);
|
|
2347
2871
|
logger.success(`Test ${id} deleted`);
|
|
2348
2872
|
});
|
|
2873
|
+
testCommand.command("run").description("Run tests locally or in the cloud").option("--local", "Run locally").option("--cloud", "Run on cloud").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>", "Cloud test ID").option("--location <location>", "Execution location (k6 cloud only)").action(async (options) => {
|
|
2874
|
+
if (options.local && options.cloud) {
|
|
2875
|
+
throw new CLIError("--local and --cloud are mutually exclusive.", 3 /* ConfigError */);
|
|
2876
|
+
}
|
|
2877
|
+
const useCloud = options.cloud || !options.local && !!options.id;
|
|
2878
|
+
if (useCloud) {
|
|
2879
|
+
if (!options.id) {
|
|
2880
|
+
throw new CLIError("Cloud runs require --id <testId>.", 3 /* ConfigError */);
|
|
2881
|
+
}
|
|
2882
|
+
const client2 = createAuthenticatedClient();
|
|
2883
|
+
const body = {};
|
|
2884
|
+
if (options.location) body.location = options.location;
|
|
2885
|
+
const { data } = await withSpinner(
|
|
2886
|
+
"Executing test",
|
|
2887
|
+
() => client2.post(`/api/tests/${options.id}/execute`, body)
|
|
2888
|
+
);
|
|
2889
|
+
outputDetail(data);
|
|
2890
|
+
return;
|
|
2891
|
+
}
|
|
2892
|
+
if (!options.file && !options.all) {
|
|
2893
|
+
throw new CLIError("Local runs require --file <path> or --all.", 3 /* ConfigError */);
|
|
2894
|
+
}
|
|
2895
|
+
const cwd = process.cwd();
|
|
2896
|
+
const { config } = await loadConfig({ cwd });
|
|
2897
|
+
const client = createAuthenticatedClient();
|
|
2898
|
+
const patterns = {
|
|
2899
|
+
playwright: config.tests?.playwright?.testMatch,
|
|
2900
|
+
k6: config.tests?.k6?.testMatch
|
|
2901
|
+
};
|
|
2902
|
+
const localTests = collectLocalTests(cwd, patterns, {
|
|
2903
|
+
file: options.file,
|
|
2904
|
+
all: options.all,
|
|
2905
|
+
type: options.type
|
|
2906
|
+
});
|
|
2907
|
+
if (localTests.length === 0) {
|
|
2908
|
+
logger.warn("No local tests found to run.");
|
|
2909
|
+
return;
|
|
2910
|
+
}
|
|
2911
|
+
await withSpinner("Validating test scripts...", async () => {
|
|
2912
|
+
await validateLocalTests(client, localTests);
|
|
2913
|
+
}, { successText: "Test scripts validated" });
|
|
2914
|
+
const playwrightTests = localTests.filter((t) => t.type === "playwright").map((t) => t.path);
|
|
2915
|
+
const k6Tests = localTests.filter((t) => t.type === "k6").map((t) => t.path);
|
|
2916
|
+
try {
|
|
2917
|
+
if (playwrightTests.length > 0) {
|
|
2918
|
+
ensureDependenciesForTestType(cwd, "playwright");
|
|
2919
|
+
}
|
|
2920
|
+
if (k6Tests.length > 0) {
|
|
2921
|
+
ensureDependenciesForTestType(cwd, "k6");
|
|
2922
|
+
}
|
|
2923
|
+
} catch (err) {
|
|
2924
|
+
if (err instanceof DependencyError) {
|
|
2925
|
+
throw new CLIError(err.message, 3 /* ConfigError */);
|
|
2926
|
+
}
|
|
2927
|
+
throw err;
|
|
2928
|
+
}
|
|
2929
|
+
if (playwrightTests.length > 0) {
|
|
2930
|
+
logger.header("Running Playwright tests");
|
|
2931
|
+
await runPlaywrightTests(playwrightTests, cwd, config.tests?.playwright?.testMatch);
|
|
2932
|
+
}
|
|
2933
|
+
if (k6Tests.length > 0) {
|
|
2934
|
+
logger.header("Running k6 tests");
|
|
2935
|
+
await runK6Tests(k6Tests, cwd);
|
|
2936
|
+
}
|
|
2937
|
+
});
|
|
2349
2938
|
testCommand.command("execute <id>").description("Execute a test immediately").option("--location <location>", "Execution location (k6 only)").action(async (id, options) => {
|
|
2350
2939
|
const client = createAuthenticatedClient();
|
|
2351
2940
|
const body = {};
|
|
@@ -2370,35 +2959,34 @@ testCommand.command("tags <id>").description("Get test tags").action(async (id)
|
|
|
2370
2959
|
]
|
|
2371
2960
|
});
|
|
2372
2961
|
});
|
|
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);
|
|
2962
|
+
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) => {
|
|
2963
|
+
const cwd = process.cwd();
|
|
2964
|
+
const filePath = resolve7(cwd, options.file);
|
|
2377
2965
|
let script;
|
|
2378
2966
|
try {
|
|
2379
2967
|
script = readFileSync4(filePath, "utf-8");
|
|
2380
2968
|
} catch {
|
|
2381
2969
|
throw new CLIError(`Cannot read file: ${filePath}`, 1 /* GeneralError */);
|
|
2382
2970
|
}
|
|
2971
|
+
const typeArg = options.type || inferTestType(options.file) || "playwright";
|
|
2383
2972
|
const client = createAuthenticatedClient();
|
|
2384
|
-
const
|
|
2973
|
+
const results = await withSpinner(
|
|
2385
2974
|
"Validating script",
|
|
2386
|
-
() => client
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2975
|
+
() => validateScripts(client, [{
|
|
2976
|
+
name: toDisplayName2(cwd, filePath),
|
|
2977
|
+
script,
|
|
2978
|
+
testType: normalizeTestTypeForApi(typeArg)
|
|
2979
|
+
}])
|
|
2390
2980
|
);
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
for (const err of data.errors) {
|
|
2397
|
-
logger.error(` - ${err}`);
|
|
2398
|
-
}
|
|
2981
|
+
const result = results[0];
|
|
2982
|
+
if (result?.valid) {
|
|
2983
|
+
logger.success(`Script is valid (${typeArg})`);
|
|
2984
|
+
if (result.warnings && result.warnings.length > 0) {
|
|
2985
|
+
logger.warn(`Warnings: ${result.warnings.join(", ")}`);
|
|
2399
2986
|
}
|
|
2400
|
-
|
|
2987
|
+
return;
|
|
2401
2988
|
}
|
|
2989
|
+
throw new CLIError(`Script validation failed: ${result?.error ?? "Unknown error"}`, 1 /* GeneralError */);
|
|
2402
2990
|
});
|
|
2403
2991
|
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
2992
|
const token = requireAuth();
|
|
@@ -2411,14 +2999,14 @@ testCommand.command("status <id>").description("Stream live status events for a
|
|
|
2411
2999
|
const controller = new AbortController();
|
|
2412
3000
|
const connectTimeout = setTimeout(() => controller.abort(), 3e4);
|
|
2413
3001
|
try {
|
|
2414
|
-
const proxy =
|
|
3002
|
+
const proxy = getProxyEnv(parsedUrl);
|
|
2415
3003
|
const response = await fetch(url, {
|
|
2416
3004
|
headers: {
|
|
2417
3005
|
"Authorization": `Bearer ${token}`,
|
|
2418
3006
|
"Accept": "text/event-stream",
|
|
2419
3007
|
"User-Agent": `supercheck-cli/${CLI_VERSION}`
|
|
2420
3008
|
},
|
|
2421
|
-
...proxy ? { dispatcher:
|
|
3009
|
+
...proxy ? { dispatcher: getProxyAgent(proxy) } : {},
|
|
2422
3010
|
signal: controller.signal
|
|
2423
3011
|
});
|
|
2424
3012
|
clearTimeout(connectTimeout);
|
|
@@ -2749,7 +3337,7 @@ tagCommand.command("delete <id>").description("Delete a tag").option("--force",
|
|
|
2749
3337
|
import { Command as Command11 } from "commander";
|
|
2750
3338
|
|
|
2751
3339
|
// src/utils/reconcile.ts
|
|
2752
|
-
import
|
|
3340
|
+
import pc4 from "picocolors";
|
|
2753
3341
|
function reconcile(local, remote) {
|
|
2754
3342
|
const changes = [];
|
|
2755
3343
|
const remoteByKey = /* @__PURE__ */ new Map();
|
|
@@ -2851,37 +3439,37 @@ function formatChangePlan(changes) {
|
|
|
2851
3439
|
logger.header("Change Plan");
|
|
2852
3440
|
logger.newline();
|
|
2853
3441
|
if (creates.length > 0) {
|
|
2854
|
-
logger.info(
|
|
3442
|
+
logger.info(pc4.green(` + ${creates.length} to create`));
|
|
2855
3443
|
for (const c of creates) {
|
|
2856
|
-
logger.info(
|
|
3444
|
+
logger.info(pc4.green(` + ${c.type}/${c.name}`));
|
|
2857
3445
|
}
|
|
2858
3446
|
}
|
|
2859
3447
|
if (updates.length > 0) {
|
|
2860
|
-
logger.info(
|
|
3448
|
+
logger.info(pc4.yellow(` ~ ${updates.length} to update`));
|
|
2861
3449
|
for (const c of updates) {
|
|
2862
|
-
logger.info(
|
|
3450
|
+
logger.info(pc4.yellow(` ~ ${c.type}/${c.name} (${c.id})`));
|
|
2863
3451
|
if (c.details) {
|
|
2864
3452
|
for (const d of c.details) {
|
|
2865
|
-
logger.info(
|
|
3453
|
+
logger.info(pc4.gray(` ${d}`));
|
|
2866
3454
|
}
|
|
2867
3455
|
}
|
|
2868
3456
|
}
|
|
2869
3457
|
}
|
|
2870
3458
|
if (deletes.length > 0) {
|
|
2871
|
-
logger.info(
|
|
3459
|
+
logger.info(pc4.red(` - ${deletes.length} to delete`));
|
|
2872
3460
|
for (const c of deletes) {
|
|
2873
|
-
logger.info(
|
|
3461
|
+
logger.info(pc4.red(` - ${c.type}/${c.name} (${c.id})`));
|
|
2874
3462
|
}
|
|
2875
3463
|
}
|
|
2876
3464
|
if (unchanged.length > 0) {
|
|
2877
|
-
logger.info(
|
|
3465
|
+
logger.info(pc4.gray(` = ${unchanged.length} unchanged`));
|
|
2878
3466
|
}
|
|
2879
3467
|
logger.newline();
|
|
2880
3468
|
const totalChanges = creates.length + updates.length + deletes.length;
|
|
2881
3469
|
if (totalChanges === 0) {
|
|
2882
3470
|
logger.success("No changes detected. Everything is in sync.");
|
|
2883
3471
|
} else {
|
|
2884
|
-
logger.info(`${
|
|
3472
|
+
logger.info(`${pc4.bold(String(totalChanges))} change(s) detected.`);
|
|
2885
3473
|
}
|
|
2886
3474
|
logger.newline();
|
|
2887
3475
|
}
|
|
@@ -2908,9 +3496,28 @@ var diffCommand = new Command11("diff").description("Preview changes between loc
|
|
|
2908
3496
|
|
|
2909
3497
|
// src/commands/deploy.ts
|
|
2910
3498
|
import { Command as Command12 } from "commander";
|
|
2911
|
-
import
|
|
3499
|
+
import { renameSync } from "fs";
|
|
3500
|
+
import { resolve as resolve8 } from "path";
|
|
3501
|
+
|
|
3502
|
+
// src/utils/paths.ts
|
|
3503
|
+
function testTypeToFolder(testType) {
|
|
3504
|
+
return testType === "k6" ? "k6" : "playwright";
|
|
3505
|
+
}
|
|
3506
|
+
function testRelativePath(testId, title, testType) {
|
|
3507
|
+
const folder = testTypeToFolder(testType);
|
|
3508
|
+
const filename = testFilename(testId, title, testType);
|
|
3509
|
+
return `_supercheck_/${folder}/${filename}`;
|
|
3510
|
+
}
|
|
3511
|
+
|
|
3512
|
+
// src/commands/deploy.ts
|
|
3513
|
+
import pc5 from "picocolors";
|
|
2912
3514
|
function prepareBodyForApi(type, body) {
|
|
2913
3515
|
const payload = { ...body };
|
|
3516
|
+
if (type === "test") {
|
|
3517
|
+
const normalizedType = normalizeTestTypeForApi(payload.testType);
|
|
3518
|
+
if (normalizedType) payload.type = normalizedType;
|
|
3519
|
+
delete payload.testType;
|
|
3520
|
+
}
|
|
2914
3521
|
if (type === "job") {
|
|
2915
3522
|
if (Array.isArray(payload.tests)) {
|
|
2916
3523
|
payload.tests = payload.tests.map((t) => {
|
|
@@ -2927,13 +3534,30 @@ function prepareBodyForApi(type, body) {
|
|
|
2927
3534
|
}
|
|
2928
3535
|
return payload;
|
|
2929
3536
|
}
|
|
2930
|
-
async function applyChange(client, change) {
|
|
3537
|
+
async function applyChange(client, change, opts) {
|
|
2931
3538
|
try {
|
|
2932
3539
|
const endpoint = getApiEndpoint(change.type);
|
|
2933
3540
|
switch (change.action) {
|
|
2934
3541
|
case "create": {
|
|
2935
|
-
const
|
|
2936
|
-
|
|
3542
|
+
const { id: _id, ...rawBody } = change.local.definition;
|
|
3543
|
+
const body = prepareBodyForApi(change.type, rawBody);
|
|
3544
|
+
const response = await client.post(endpoint, body);
|
|
3545
|
+
if (change.type === "test") {
|
|
3546
|
+
const responseBody = response.data;
|
|
3547
|
+
const createdId = responseBody?.test?.id;
|
|
3548
|
+
const createdTitle = responseBody?.test?.title ?? change.local?.definition?.title;
|
|
3549
|
+
const testType = change.local?.definition?.testType === "k6" ? "k6" : "playwright";
|
|
3550
|
+
if (createdId && typeof createdTitle === "string") {
|
|
3551
|
+
const existingPath = opts.testFilesByName.get(change.name);
|
|
3552
|
+
if (existingPath) {
|
|
3553
|
+
const newRelPath = testRelativePath(createdId, createdTitle, testType);
|
|
3554
|
+
const newPath = resolve8(process.cwd(), newRelPath);
|
|
3555
|
+
if (newPath !== existingPath) {
|
|
3556
|
+
renameSync(existingPath, newPath);
|
|
3557
|
+
}
|
|
3558
|
+
}
|
|
3559
|
+
}
|
|
3560
|
+
}
|
|
2937
3561
|
return { success: true };
|
|
2938
3562
|
}
|
|
2939
3563
|
case "update": {
|
|
@@ -2983,9 +3607,30 @@ var deployCommand = new Command12("deploy").description("Push local config resou
|
|
|
2983
3607
|
}
|
|
2984
3608
|
formatChangePlan(changes);
|
|
2985
3609
|
if (options.dryRun) {
|
|
2986
|
-
logger.info(
|
|
3610
|
+
logger.info(pc5.yellow("Dry run \u2014 no changes applied."));
|
|
2987
3611
|
return;
|
|
2988
3612
|
}
|
|
3613
|
+
const testsToValidate = actionable.filter((c) => c.type === "test" && (c.action === "create" || c.action === "update"));
|
|
3614
|
+
if (testsToValidate.length > 0) {
|
|
3615
|
+
await withSpinner("Validating test scripts...", async () => {
|
|
3616
|
+
const inputs = testsToValidate.map((change) => ({
|
|
3617
|
+
name: change.name,
|
|
3618
|
+
script: String(change.local?.definition?.script ?? ""),
|
|
3619
|
+
testType: normalizeTestTypeForApi(change.local?.definition?.testType)
|
|
3620
|
+
}));
|
|
3621
|
+
const results = await validateScripts(client, inputs);
|
|
3622
|
+
const failures = results.filter((r) => !r.valid);
|
|
3623
|
+
if (failures.length > 0) {
|
|
3624
|
+
const details = failures.map((f) => ` - ${f.name}: ${f.error ?? "Validation failed"}`).join("\n");
|
|
3625
|
+
throw new CLIError(`Validation failed:
|
|
3626
|
+
${details}`, 3 /* ConfigError */);
|
|
3627
|
+
}
|
|
3628
|
+
const warnings = results.filter((r) => r.warnings && r.warnings.length > 0);
|
|
3629
|
+
for (const warn of warnings) {
|
|
3630
|
+
logger.warn(`Validation warnings for ${warn.name}: ${(warn.warnings ?? []).join(", ")}`);
|
|
3631
|
+
}
|
|
3632
|
+
}, { successText: "Test scripts validated" });
|
|
3633
|
+
}
|
|
2989
3634
|
if (!options.force) {
|
|
2990
3635
|
const confirmed = await confirmPrompt("Apply these changes?", { default: false });
|
|
2991
3636
|
if (!confirmed) {
|
|
@@ -3002,10 +3647,19 @@ var deployCommand = new Command12("deploy").description("Push local config resou
|
|
|
3002
3647
|
...actionable.filter((c) => c.action === "update"),
|
|
3003
3648
|
...actionable.filter((c) => c.action === "delete")
|
|
3004
3649
|
];
|
|
3650
|
+
const testFilesByName = /* @__PURE__ */ new Map();
|
|
3651
|
+
const patterns = {
|
|
3652
|
+
playwright: config.tests?.playwright?.testMatch,
|
|
3653
|
+
k6: config.tests?.k6?.testMatch
|
|
3654
|
+
};
|
|
3655
|
+
for (const file of discoverFiles(cwd, patterns)) {
|
|
3656
|
+
const key = file.relativePath.replace(/^_supercheck_\//, "");
|
|
3657
|
+
testFilesByName.set(key, file.absolutePath);
|
|
3658
|
+
}
|
|
3005
3659
|
for (const change of ordered) {
|
|
3006
3660
|
const actionLabel = change.action === "create" ? "+" : change.action === "update" ? "~" : "-";
|
|
3007
|
-
const color = change.action === "create" ?
|
|
3008
|
-
const result = await applyChange(client, change);
|
|
3661
|
+
const color = change.action === "create" ? pc5.green : change.action === "update" ? pc5.yellow : pc5.red;
|
|
3662
|
+
const result = await applyChange(client, change, { testFilesByName });
|
|
3009
3663
|
if (result.success) {
|
|
3010
3664
|
logger.info(color(` ${actionLabel} ${change.type}/${change.name} \u2713`));
|
|
3011
3665
|
succeeded++;
|
|
@@ -3027,7 +3681,7 @@ var deployCommand = new Command12("deploy").description("Push local config resou
|
|
|
3027
3681
|
|
|
3028
3682
|
// src/commands/destroy.ts
|
|
3029
3683
|
import { Command as Command13 } from "commander";
|
|
3030
|
-
import
|
|
3684
|
+
import pc6 from "picocolors";
|
|
3031
3685
|
async function fetchManagedResources(client, config) {
|
|
3032
3686
|
const resources = [];
|
|
3033
3687
|
const managedIds = /* @__PURE__ */ new Set();
|
|
@@ -3157,13 +3811,13 @@ var destroyCommand = new Command13("destroy").description("Tear down managed res
|
|
|
3157
3811
|
logger.header("Resources to destroy:");
|
|
3158
3812
|
logger.newline();
|
|
3159
3813
|
for (const r of managed) {
|
|
3160
|
-
logger.info(
|
|
3814
|
+
logger.info(pc6.red(` - ${r.type}/${r.name} [${r.id}]`));
|
|
3161
3815
|
}
|
|
3162
3816
|
logger.newline();
|
|
3163
|
-
logger.warn(`This will permanently delete ${
|
|
3817
|
+
logger.warn(`This will permanently delete ${pc6.bold(String(managed.length))} resource(s).`);
|
|
3164
3818
|
logger.newline();
|
|
3165
3819
|
if (options.dryRun) {
|
|
3166
|
-
logger.info(
|
|
3820
|
+
logger.info(pc6.yellow("Dry run \u2014 no resources destroyed."));
|
|
3167
3821
|
return;
|
|
3168
3822
|
}
|
|
3169
3823
|
if (!options.force) {
|
|
@@ -3182,7 +3836,7 @@ var destroyCommand = new Command13("destroy").description("Tear down managed res
|
|
|
3182
3836
|
try {
|
|
3183
3837
|
const endpoint = getApiEndpoint(resource.type);
|
|
3184
3838
|
await client.delete(`${endpoint}/${resource.id}`);
|
|
3185
|
-
logger.info(
|
|
3839
|
+
logger.info(pc6.red(` - ${resource.type}/${resource.name} \u2713`));
|
|
3186
3840
|
succeeded++;
|
|
3187
3841
|
} catch (err) {
|
|
3188
3842
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -3203,30 +3857,23 @@ var destroyCommand = new Command13("destroy").description("Tear down managed res
|
|
|
3203
3857
|
|
|
3204
3858
|
// src/commands/pull.ts
|
|
3205
3859
|
import { Command as Command14 } from "commander";
|
|
3206
|
-
import { existsSync as
|
|
3207
|
-
import { resolve as
|
|
3208
|
-
import
|
|
3860
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync2, writeFileSync as writeFileSync4, readFileSync as readFileSync5, readdirSync as readdirSync2, unlinkSync } from "fs";
|
|
3861
|
+
import { resolve as resolve9, dirname as dirname2 } from "path";
|
|
3862
|
+
import pc7 from "picocolors";
|
|
3209
3863
|
function mapTestType(apiType) {
|
|
3210
3864
|
if (apiType === "performance" || apiType === "k6") return "k6";
|
|
3211
3865
|
return "playwright";
|
|
3212
3866
|
}
|
|
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
3867
|
function writeIfChanged(filePath, content) {
|
|
3221
|
-
if (
|
|
3222
|
-
const existing =
|
|
3868
|
+
if (existsSync5(filePath)) {
|
|
3869
|
+
const existing = readFileSync5(filePath, "utf-8");
|
|
3223
3870
|
if (existing === content) return false;
|
|
3224
3871
|
}
|
|
3225
3872
|
const dir = dirname2(filePath);
|
|
3226
|
-
if (!
|
|
3873
|
+
if (!existsSync5(dir)) {
|
|
3227
3874
|
mkdirSync2(dir, { recursive: true });
|
|
3228
3875
|
}
|
|
3229
|
-
|
|
3876
|
+
writeFileSync4(filePath, content, "utf-8");
|
|
3230
3877
|
return true;
|
|
3231
3878
|
}
|
|
3232
3879
|
function decodeScript(script) {
|
|
@@ -3310,25 +3957,60 @@ async function fetchStatusPages(client) {
|
|
|
3310
3957
|
}
|
|
3311
3958
|
}
|
|
3312
3959
|
function pullTests(tests, cwd, summary) {
|
|
3313
|
-
const
|
|
3314
|
-
if (!
|
|
3315
|
-
mkdirSync2(
|
|
3960
|
+
const baseDir = resolve9(cwd, "_supercheck_");
|
|
3961
|
+
if (!existsSync5(baseDir)) {
|
|
3962
|
+
mkdirSync2(baseDir, { recursive: true });
|
|
3963
|
+
}
|
|
3964
|
+
const existingByUuid = /* @__PURE__ */ new Map();
|
|
3965
|
+
try {
|
|
3966
|
+
for (const folder of ["playwright", "k6"]) {
|
|
3967
|
+
const folderPath = resolve9(baseDir, folder);
|
|
3968
|
+
if (!existsSync5(folderPath)) continue;
|
|
3969
|
+
for (const file of readdirSync2(folderPath)) {
|
|
3970
|
+
const uuid = extractUuidFromFilename(file);
|
|
3971
|
+
if (uuid) existingByUuid.set(uuid, `${folder}/${file}`);
|
|
3972
|
+
}
|
|
3973
|
+
}
|
|
3974
|
+
} catch {
|
|
3316
3975
|
}
|
|
3317
3976
|
for (const test of tests) {
|
|
3318
3977
|
try {
|
|
3319
3978
|
const testType = mapTestType(test.type);
|
|
3320
|
-
const
|
|
3321
|
-
const
|
|
3322
|
-
|
|
3979
|
+
const relPath = testRelativePath(test.id, test.title, testType);
|
|
3980
|
+
const folderPath = resolve9(baseDir, testTypeToFolder(testType));
|
|
3981
|
+
if (!existsSync5(folderPath)) {
|
|
3982
|
+
mkdirSync2(folderPath, { recursive: true });
|
|
3983
|
+
}
|
|
3984
|
+
let script = decodeScript(test.script);
|
|
3323
3985
|
if (!script) {
|
|
3324
3986
|
logger.debug(`Skipping test "${test.title}" \u2014 no script content`);
|
|
3325
3987
|
summary.skipped++;
|
|
3326
3988
|
continue;
|
|
3327
3989
|
}
|
|
3328
|
-
|
|
3990
|
+
if (/@title\s+/.test(script)) {
|
|
3991
|
+
script = script.replace(/(@title\s+)(.+?)(\r?\n|\*\/|$)/, `$1${test.title}$3`);
|
|
3992
|
+
} else if (/\/\*\*([\s\S]*?)\*\//.test(script)) {
|
|
3993
|
+
script = script.replace(/(\/\*\*)/, `$1
|
|
3994
|
+
* @title ${test.title}`);
|
|
3995
|
+
} else {
|
|
3996
|
+
script = `// @title ${test.title}
|
|
3997
|
+
|
|
3998
|
+
${script}`;
|
|
3999
|
+
}
|
|
4000
|
+
const existingFilename = existingByUuid.get(test.id);
|
|
4001
|
+
const currentFile = relPath.replace(/^_supercheck_\//, "");
|
|
4002
|
+
if (existingFilename && existingFilename !== currentFile) {
|
|
4003
|
+
const oldPath = resolve9(baseDir, existingFilename);
|
|
4004
|
+
try {
|
|
4005
|
+
unlinkSync(oldPath);
|
|
4006
|
+
logger.debug(` Renamed: ${existingFilename} \u2192 ${currentFile}`);
|
|
4007
|
+
} catch {
|
|
4008
|
+
}
|
|
4009
|
+
}
|
|
4010
|
+
const filePath = resolve9(cwd, relPath);
|
|
3329
4011
|
const written = writeIfChanged(filePath, script);
|
|
3330
4012
|
if (written) {
|
|
3331
|
-
logger.info(
|
|
4013
|
+
logger.info(pc7.green(` + ${relPath}`));
|
|
3332
4014
|
summary.tests++;
|
|
3333
4015
|
} else {
|
|
3334
4016
|
logger.debug(` = ${relPath} (unchanged)`);
|
|
@@ -3483,10 +4165,10 @@ function generateConfigContent(opts) {
|
|
|
3483
4165
|
parts.push(" // Test file patterns \u2014 Playwright (.pw.ts) and k6 (.k6.ts) scripts");
|
|
3484
4166
|
parts.push(" tests: {");
|
|
3485
4167
|
parts.push(" playwright: {");
|
|
3486
|
-
parts.push(` testMatch: '_supercheck_/
|
|
4168
|
+
parts.push(` testMatch: '_supercheck_/playwright/**/*.pw.ts',`);
|
|
3487
4169
|
parts.push(" },");
|
|
3488
4170
|
parts.push(" k6: {");
|
|
3489
|
-
parts.push(` testMatch: '_supercheck_/
|
|
4171
|
+
parts.push(` testMatch: '_supercheck_/k6/**/*.k6.ts',`);
|
|
3490
4172
|
parts.push(" },");
|
|
3491
4173
|
parts.push(" },");
|
|
3492
4174
|
if (opts.monitors.length > 0) {
|
|
@@ -3529,7 +4211,9 @@ function generateConfigContent(opts) {
|
|
|
3529
4211
|
parts.push(" * npm install -D @supercheck/cli typescript @types/node");
|
|
3530
4212
|
parts.push(" * # If using Playwright tests, also install:");
|
|
3531
4213
|
parts.push(" * npm install -D @playwright/test");
|
|
3532
|
-
parts.push(" * # If using k6 tests, install k6:
|
|
4214
|
+
parts.push(" * # If using k6 tests, also install types and k6 runtime:");
|
|
4215
|
+
parts.push(" * npm install -D @types/k6");
|
|
4216
|
+
parts.push(" * # Install k6 runtime: https://grafana.com/docs/k6/latest/set-up/install-k6/");
|
|
3533
4217
|
parts.push(" *");
|
|
3534
4218
|
parts.push(" * 2. Review the configuration above and make any changes you need.");
|
|
3535
4219
|
parts.push(" * 3. Preview what will change: npx supercheck diff");
|
|
@@ -3654,39 +4338,39 @@ var pullCommand = new Command14("pull").description("Pull tests, monitors, jobs,
|
|
|
3654
4338
|
logger.header("Resources that would be pulled:");
|
|
3655
4339
|
logger.newline();
|
|
3656
4340
|
if (tests.length > 0) {
|
|
3657
|
-
logger.info(
|
|
4341
|
+
logger.info(pc7.cyan(` Tests (${tests.length}):`));
|
|
3658
4342
|
for (const t of tests) {
|
|
3659
4343
|
const testType = mapTestType(t.type);
|
|
3660
4344
|
logger.info(` ${t.title} (${testType})`);
|
|
3661
4345
|
}
|
|
3662
4346
|
}
|
|
3663
4347
|
if (monitors.length > 0) {
|
|
3664
|
-
logger.info(
|
|
4348
|
+
logger.info(pc7.cyan(` Monitors (${monitors.length}):`));
|
|
3665
4349
|
for (const m of monitors) logger.info(` ${m.name} (${m.type}, every ${m.frequencyMinutes ?? "?"}min)`);
|
|
3666
4350
|
}
|
|
3667
4351
|
if (jobs.length > 0) {
|
|
3668
|
-
logger.info(
|
|
4352
|
+
logger.info(pc7.cyan(` Jobs (${jobs.length}):`));
|
|
3669
4353
|
for (const j of jobs) logger.info(` ${j.name}${j.cronSchedule ? ` (${j.cronSchedule})` : ""}`);
|
|
3670
4354
|
}
|
|
3671
4355
|
if (variables.length > 0) {
|
|
3672
|
-
logger.info(
|
|
4356
|
+
logger.info(pc7.cyan(` Variables (${variables.length}):`));
|
|
3673
4357
|
for (const v of variables) logger.info(` ${v.key}${v.isSecret ? " (secret)" : ""}`);
|
|
3674
4358
|
}
|
|
3675
4359
|
if (tags.length > 0) {
|
|
3676
|
-
logger.info(
|
|
4360
|
+
logger.info(pc7.cyan(` Tags (${tags.length}):`));
|
|
3677
4361
|
for (const t of tags) logger.info(` ${t.name}`);
|
|
3678
4362
|
}
|
|
3679
4363
|
if (statusPages.length > 0) {
|
|
3680
|
-
logger.info(
|
|
4364
|
+
logger.info(pc7.cyan(` Status Pages (${statusPages.length}):`));
|
|
3681
4365
|
for (const sp of statusPages) logger.info(` ${sp.name} (${sp.status ?? "draft"})`);
|
|
3682
4366
|
}
|
|
3683
4367
|
logger.newline();
|
|
3684
|
-
logger.info(
|
|
4368
|
+
logger.info(pc7.yellow("Dry run \u2014 no files written."));
|
|
3685
4369
|
logger.newline();
|
|
3686
4370
|
return;
|
|
3687
4371
|
}
|
|
3688
4372
|
if (!options.force) {
|
|
3689
|
-
logger.info(`Found ${
|
|
4373
|
+
logger.info(`Found ${pc7.bold(String(totalResources))} resources to pull.`);
|
|
3690
4374
|
logger.info("This will write test scripts and update supercheck.config.ts.");
|
|
3691
4375
|
logger.newline();
|
|
3692
4376
|
const { confirmPrompt: confirmPrompt2 } = await import("../prompt-BPDPYRS7.js");
|
|
@@ -3733,11 +4417,52 @@ var pullCommand = new Command14("pull").description("Pull tests, monitors, jobs,
|
|
|
3733
4417
|
pullTests(fullTests, cwd, summary);
|
|
3734
4418
|
logger.newline();
|
|
3735
4419
|
}
|
|
4420
|
+
const pkgPath = resolve9(cwd, "package.json");
|
|
4421
|
+
if (!options.dryRun && !existsSync5(pkgPath)) {
|
|
4422
|
+
logger.newline();
|
|
4423
|
+
logger.info("Initializing project dependencies...");
|
|
4424
|
+
const pm = detectPackageManager(cwd);
|
|
4425
|
+
ensurePackageJson(cwd);
|
|
4426
|
+
const packages = ["@supercheck/cli", "typescript", "@types/node"];
|
|
4427
|
+
const hasPlaywright = tests.some((t) => mapTestType(t.type) === "playwright");
|
|
4428
|
+
const hasK6 = tests.some((t) => mapTestType(t.type) === "k6");
|
|
4429
|
+
if (hasPlaywright) packages.push("@playwright/test");
|
|
4430
|
+
if (hasK6) packages.push("@types/k6");
|
|
4431
|
+
await installDependencies(cwd, pm, {
|
|
4432
|
+
packages,
|
|
4433
|
+
skipInstall: false
|
|
4434
|
+
});
|
|
4435
|
+
const tsconfigPath = resolve9(cwd, "tsconfig.supercheck.json");
|
|
4436
|
+
if (!existsSync5(tsconfigPath)) {
|
|
4437
|
+
const tsconfigContent = JSON.stringify({
|
|
4438
|
+
compilerOptions: {
|
|
4439
|
+
target: "ES2022",
|
|
4440
|
+
module: "ESNext",
|
|
4441
|
+
moduleResolution: "bundler",
|
|
4442
|
+
esModuleInterop: true,
|
|
4443
|
+
strict: true,
|
|
4444
|
+
skipLibCheck: true,
|
|
4445
|
+
resolveJsonModule: true,
|
|
4446
|
+
isolatedModules: true,
|
|
4447
|
+
noEmit: true,
|
|
4448
|
+
types: ["node", ...hasK6 ? ["k6"] : []]
|
|
4449
|
+
},
|
|
4450
|
+
include: [
|
|
4451
|
+
"supercheck.config.ts",
|
|
4452
|
+
"supercheck.config.local.ts",
|
|
4453
|
+
"_supercheck_/**/*.ts"
|
|
4454
|
+
]
|
|
4455
|
+
}, null, 2) + "\n";
|
|
4456
|
+
writeFileSync4(tsconfigPath, tsconfigContent, "utf-8");
|
|
4457
|
+
logger.success("Created tsconfig.supercheck.json (IDE IntelliSense)");
|
|
4458
|
+
}
|
|
4459
|
+
logger.success("Project initialized with dependencies");
|
|
4460
|
+
}
|
|
3736
4461
|
if (!options.testsOnly) {
|
|
3737
4462
|
const testMap = /* @__PURE__ */ new Map();
|
|
3738
4463
|
for (const t of tests) {
|
|
3739
4464
|
const testType = mapTestType(t.type);
|
|
3740
|
-
const filePath =
|
|
4465
|
+
const filePath = testRelativePath(t.id, t.title, testType);
|
|
3741
4466
|
testMap.set(t.id, filePath);
|
|
3742
4467
|
}
|
|
3743
4468
|
const monitorDefs = buildMonitorDefinitions(monitors);
|
|
@@ -3766,15 +4491,15 @@ var pullCommand = new Command14("pull").description("Pull tests, monitors, jobs,
|
|
|
3766
4491
|
tags: tagDefs,
|
|
3767
4492
|
statusPages: statusPageDefs
|
|
3768
4493
|
});
|
|
3769
|
-
const configPath =
|
|
4494
|
+
const configPath = resolve9(cwd, "supercheck.config.ts");
|
|
3770
4495
|
const configChanged = writeIfChanged(configPath, configContent);
|
|
3771
4496
|
if (configChanged) {
|
|
3772
4497
|
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(
|
|
4498
|
+
if (monitorDefs.length > 0) logger.info(pc7.green(` + ${monitorDefs.length} monitor(s)`));
|
|
4499
|
+
if (jobDefs.length > 0) logger.info(pc7.green(` + ${jobDefs.length} job(s)`));
|
|
4500
|
+
if (variableDefs.length > 0) logger.info(pc7.green(` + ${variableDefs.length} variable(s)`));
|
|
4501
|
+
if (tagDefs.length > 0) logger.info(pc7.green(` + ${tagDefs.length} tag(s)`));
|
|
4502
|
+
if (statusPageDefs.length > 0) logger.info(pc7.green(` + ${statusPageDefs.length} status page(s)`));
|
|
3778
4503
|
logger.success("Updated supercheck.config.ts");
|
|
3779
4504
|
} else {
|
|
3780
4505
|
logger.info("supercheck.config.ts is already up to date");
|
|
@@ -3815,9 +4540,53 @@ var pullCommand = new Command14("pull").description("Pull tests, monitors, jobs,
|
|
|
3815
4540
|
logger.newline();
|
|
3816
4541
|
});
|
|
3817
4542
|
|
|
3818
|
-
// src/commands/
|
|
4543
|
+
// src/commands/validate.ts
|
|
3819
4544
|
import { Command as Command15 } from "commander";
|
|
3820
|
-
|
|
4545
|
+
import pc8 from "picocolors";
|
|
4546
|
+
var validateCommand = new Command15("validate").description("Validate local test scripts against Supercheck rules").option("--config <path>", "Path to config file").action(async (options) => {
|
|
4547
|
+
const cwd = process.cwd();
|
|
4548
|
+
const { config } = await loadConfig({ cwd, configPath: options.config });
|
|
4549
|
+
const client = createAuthenticatedClient();
|
|
4550
|
+
const localResources = buildLocalResources(config, cwd);
|
|
4551
|
+
const tests = localResources.filter((r) => r.type === "test");
|
|
4552
|
+
if (tests.length === 0) {
|
|
4553
|
+
logger.warn("No local tests found to validate.");
|
|
4554
|
+
return;
|
|
4555
|
+
}
|
|
4556
|
+
const inputs = tests.map((test) => ({
|
|
4557
|
+
name: test.name,
|
|
4558
|
+
script: String(test.definition?.script ?? ""),
|
|
4559
|
+
testType: normalizeTestTypeForApi(test.definition?.testType)
|
|
4560
|
+
}));
|
|
4561
|
+
const results = await withSpinner("Validating test scripts...", async () => {
|
|
4562
|
+
return validateScripts(client, inputs);
|
|
4563
|
+
}, { successText: "Validation complete" });
|
|
4564
|
+
logger.newline();
|
|
4565
|
+
logger.header("Validation Results");
|
|
4566
|
+
logger.newline();
|
|
4567
|
+
let failed = 0;
|
|
4568
|
+
for (const result of results) {
|
|
4569
|
+
if (result.valid) {
|
|
4570
|
+
logger.info(pc8.green(` \u2713 ${result.name}`));
|
|
4571
|
+
if (result.warnings && result.warnings.length > 0) {
|
|
4572
|
+
logger.info(pc8.yellow(` Warnings: ${result.warnings.join(", ")}`));
|
|
4573
|
+
}
|
|
4574
|
+
} else {
|
|
4575
|
+
failed++;
|
|
4576
|
+
logger.info(pc8.red(` \u2717 ${result.name}`));
|
|
4577
|
+
logger.info(pc8.gray(` ${result.error ?? "Validation failed"}`));
|
|
4578
|
+
}
|
|
4579
|
+
}
|
|
4580
|
+
logger.newline();
|
|
4581
|
+
if (failed > 0) {
|
|
4582
|
+
throw new CLIError(`Validation failed for ${failed} test(s).`, 3 /* ConfigError */);
|
|
4583
|
+
}
|
|
4584
|
+
logger.success("All tests passed validation.");
|
|
4585
|
+
});
|
|
4586
|
+
|
|
4587
|
+
// src/commands/notifications.ts
|
|
4588
|
+
import { Command as Command16 } from "commander";
|
|
4589
|
+
var notificationCommand = new Command16("notification").alias("notifications").description("Manage notification providers");
|
|
3821
4590
|
notificationCommand.command("list").description("List notification providers").action(async () => {
|
|
3822
4591
|
const client = createAuthenticatedClient();
|
|
3823
4592
|
const { data } = await withSpinner(
|
|
@@ -3949,8 +4718,8 @@ notificationCommand.command("test").description("Send a test notification to ver
|
|
|
3949
4718
|
});
|
|
3950
4719
|
|
|
3951
4720
|
// src/commands/alerts.ts
|
|
3952
|
-
import { Command as
|
|
3953
|
-
var alertCommand = new
|
|
4721
|
+
import { Command as Command17 } from "commander";
|
|
4722
|
+
var alertCommand = new Command17("alert").alias("alerts").description("View alert history");
|
|
3954
4723
|
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
4724
|
const client = createAuthenticatedClient();
|
|
3956
4725
|
const { data } = await withSpinner(
|
|
@@ -3978,8 +4747,8 @@ Page ${pagination.page}/${pagination.totalPages} (${pagination.total} total)`
|
|
|
3978
4747
|
});
|
|
3979
4748
|
|
|
3980
4749
|
// src/commands/audit.ts
|
|
3981
|
-
import { Command as
|
|
3982
|
-
var auditCommand = new
|
|
4750
|
+
import { Command as Command18 } from "commander";
|
|
4751
|
+
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
4752
|
const client = createAuthenticatedClient();
|
|
3984
4753
|
const params = {
|
|
3985
4754
|
page: options.page,
|
|
@@ -4017,8 +4786,227 @@ Page ${pagination.currentPage}/${pagination.totalPages} (${pagination.totalCount
|
|
|
4017
4786
|
}
|
|
4018
4787
|
});
|
|
4019
4788
|
|
|
4789
|
+
// src/commands/doctor.ts
|
|
4790
|
+
import { Command as Command19 } from "commander";
|
|
4791
|
+
import pc9 from "picocolors";
|
|
4792
|
+
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) => {
|
|
4793
|
+
const cwd = process.cwd();
|
|
4794
|
+
const format = getOutputFormat();
|
|
4795
|
+
logger.newline();
|
|
4796
|
+
logger.header("Supercheck Doctor");
|
|
4797
|
+
logger.newline();
|
|
4798
|
+
logger.info(pc9.bold("Dependencies:"));
|
|
4799
|
+
const deps = checkAllDependencies(cwd);
|
|
4800
|
+
if (format === "json") {
|
|
4801
|
+
output(deps, {
|
|
4802
|
+
columns: [
|
|
4803
|
+
{ key: "name", header: "Dependency" },
|
|
4804
|
+
{ key: "installed", header: "Installed" },
|
|
4805
|
+
{ key: "version", header: "Version" },
|
|
4806
|
+
{ key: "required", header: "Required" },
|
|
4807
|
+
{ key: "installHint", header: "Install" }
|
|
4808
|
+
]
|
|
4809
|
+
});
|
|
4810
|
+
} else {
|
|
4811
|
+
logger.output(formatDependencyReport(deps));
|
|
4812
|
+
}
|
|
4813
|
+
logger.newline();
|
|
4814
|
+
logger.info(pc9.bold("Authentication:"));
|
|
4815
|
+
const hasAuth = isAuthenticated();
|
|
4816
|
+
if (hasAuth) {
|
|
4817
|
+
const baseUrl = getStoredBaseUrl() ?? "https://app.supercheck.io";
|
|
4818
|
+
logger.output(` ${pc9.green("\u2713")} Authenticated (${pc9.dim(baseUrl)})`);
|
|
4819
|
+
} else {
|
|
4820
|
+
logger.output(` ${pc9.yellow("\u25CB")} Not authenticated \u2014 run: supercheck login --token <token>`);
|
|
4821
|
+
}
|
|
4822
|
+
logger.newline();
|
|
4823
|
+
logger.info(pc9.bold("Configuration:"));
|
|
4824
|
+
const configResult = await tryLoadConfig();
|
|
4825
|
+
if (configResult) {
|
|
4826
|
+
logger.output(` ${pc9.green("\u2713")} supercheck.config.ts found`);
|
|
4827
|
+
const org = configResult.config.project?.organization;
|
|
4828
|
+
const proj = configResult.config.project?.project;
|
|
4829
|
+
if (org && proj) {
|
|
4830
|
+
logger.output(` ${pc9.green("\u2713")} Project: ${pc9.dim(`${org}/${proj}`)}`);
|
|
4831
|
+
} else {
|
|
4832
|
+
logger.output(` ${pc9.yellow("\u25CB")} Project org/project not configured in supercheck.config.ts`);
|
|
4833
|
+
}
|
|
4834
|
+
} else {
|
|
4835
|
+
logger.output(` ${pc9.yellow("\u25CB")} No supercheck.config.ts \u2014 run: supercheck init`);
|
|
4836
|
+
}
|
|
4837
|
+
logger.newline();
|
|
4838
|
+
const missingRequired = deps.filter((d) => !d.installed && d.required);
|
|
4839
|
+
if (options.fix && missingRequired.length > 0) {
|
|
4840
|
+
logger.header("Attempting to fix missing dependencies...");
|
|
4841
|
+
logger.newline();
|
|
4842
|
+
for (const dep of missingRequired) {
|
|
4843
|
+
if (dep.name === "Playwright browsers") {
|
|
4844
|
+
const ok = await installPlaywrightBrowsers(cwd);
|
|
4845
|
+
if (ok) {
|
|
4846
|
+
logger.success("Playwright browsers installed");
|
|
4847
|
+
} else {
|
|
4848
|
+
logger.error(`Failed to install Playwright browsers. Run manually: ${dep.installHint}`);
|
|
4849
|
+
}
|
|
4850
|
+
} else if (dep.name === "@playwright/test") {
|
|
4851
|
+
logger.info(`Install @playwright/test:`);
|
|
4852
|
+
logger.info(` ${dep.installHint}`);
|
|
4853
|
+
}
|
|
4854
|
+
}
|
|
4855
|
+
logger.newline();
|
|
4856
|
+
}
|
|
4857
|
+
if (missingRequired.length === 0 && hasAuth && configResult) {
|
|
4858
|
+
logger.success("Everything looks good! You're ready to use Supercheck.");
|
|
4859
|
+
} else {
|
|
4860
|
+
const issues = [];
|
|
4861
|
+
if (missingRequired.length > 0) {
|
|
4862
|
+
issues.push(`${missingRequired.length} missing required dependency(s)`);
|
|
4863
|
+
}
|
|
4864
|
+
if (!hasAuth) {
|
|
4865
|
+
issues.push("not authenticated");
|
|
4866
|
+
}
|
|
4867
|
+
if (!configResult) {
|
|
4868
|
+
issues.push("no config file");
|
|
4869
|
+
}
|
|
4870
|
+
logger.warn(`Issues found: ${issues.join(", ")}`);
|
|
4871
|
+
if (!options.fix && missingRequired.length > 0) {
|
|
4872
|
+
logger.info(`Run ${pc9.bold("supercheck doctor --fix")} to attempt automatic fixes.`);
|
|
4873
|
+
}
|
|
4874
|
+
}
|
|
4875
|
+
logger.newline();
|
|
4876
|
+
if (missingRequired.length > 0) {
|
|
4877
|
+
throw new CLIError(
|
|
4878
|
+
`Missing ${missingRequired.length} required dependency(s). Run 'supercheck doctor --fix' or install manually.`,
|
|
4879
|
+
3 /* ConfigError */
|
|
4880
|
+
);
|
|
4881
|
+
}
|
|
4882
|
+
});
|
|
4883
|
+
|
|
4884
|
+
// src/commands/upgrade.ts
|
|
4885
|
+
import { Command as Command20 } from "commander";
|
|
4886
|
+
import { spawn as spawn2 } from "child_process";
|
|
4887
|
+
var PACKAGE_NAME = "@supercheck/cli";
|
|
4888
|
+
var TAG_PATTERN = /^[a-z0-9][a-z0-9._-]*$/i;
|
|
4889
|
+
function normalizeTag(tag) {
|
|
4890
|
+
const trimmed = tag.trim();
|
|
4891
|
+
if (trimmed.startsWith("v") && trimmed.length > 1 && /\d/.test(trimmed[1])) {
|
|
4892
|
+
return trimmed.slice(1);
|
|
4893
|
+
}
|
|
4894
|
+
return trimmed;
|
|
4895
|
+
}
|
|
4896
|
+
function getDefaultTag() {
|
|
4897
|
+
return "latest";
|
|
4898
|
+
}
|
|
4899
|
+
function detectPackageManager2() {
|
|
4900
|
+
const userAgent = process.env.npm_config_user_agent ?? "";
|
|
4901
|
+
if (userAgent.includes("pnpm")) return "pnpm";
|
|
4902
|
+
if (userAgent.includes("yarn")) return "yarn";
|
|
4903
|
+
if (userAgent.includes("bun")) return "bun";
|
|
4904
|
+
return "npm";
|
|
4905
|
+
}
|
|
4906
|
+
function buildInstallCommand(pm, pkgSpec) {
|
|
4907
|
+
switch (pm) {
|
|
4908
|
+
case "yarn": {
|
|
4909
|
+
const args = ["global", "add", pkgSpec];
|
|
4910
|
+
return { command: "yarn", args, preview: `yarn ${args.join(" ")}` };
|
|
4911
|
+
}
|
|
4912
|
+
case "pnpm": {
|
|
4913
|
+
const args = ["add", "-g", pkgSpec];
|
|
4914
|
+
return { command: "pnpm", args, preview: `pnpm ${args.join(" ")}` };
|
|
4915
|
+
}
|
|
4916
|
+
case "bun": {
|
|
4917
|
+
const args = ["add", "-g", pkgSpec];
|
|
4918
|
+
return { command: "bun", args, preview: `bun ${args.join(" ")}` };
|
|
4919
|
+
}
|
|
4920
|
+
case "npm":
|
|
4921
|
+
default: {
|
|
4922
|
+
const args = ["install", "-g", pkgSpec];
|
|
4923
|
+
return { command: "npm", args, preview: `npm ${args.join(" ")}` };
|
|
4924
|
+
}
|
|
4925
|
+
}
|
|
4926
|
+
}
|
|
4927
|
+
function runInstallCommand(command, args) {
|
|
4928
|
+
return new Promise((resolve10, reject) => {
|
|
4929
|
+
const child = spawn2(command, args, { stdio: "inherit" });
|
|
4930
|
+
child.on("error", (err) => reject(err));
|
|
4931
|
+
child.on("close", (code) => resolve10(code ?? 0));
|
|
4932
|
+
});
|
|
4933
|
+
}
|
|
4934
|
+
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) => {
|
|
4935
|
+
const format = getOutputFormat();
|
|
4936
|
+
const tag = normalizeTag(options.tag ?? getDefaultTag());
|
|
4937
|
+
if (!TAG_PATTERN.test(tag)) {
|
|
4938
|
+
throw new CLIError(
|
|
4939
|
+
`Invalid tag "${tag}". Use a valid dist-tag (latest, next, canary) or version (0.1.1-beta.1).`,
|
|
4940
|
+
3 /* ConfigError */
|
|
4941
|
+
);
|
|
4942
|
+
}
|
|
4943
|
+
const pm = options.packageManager ?? detectPackageManager2();
|
|
4944
|
+
if (!["npm", "yarn", "pnpm", "bun"].includes(pm)) {
|
|
4945
|
+
throw new CLIError(
|
|
4946
|
+
`Unsupported package manager "${pm}". Use npm, yarn, pnpm, or bun.`,
|
|
4947
|
+
3 /* ConfigError */
|
|
4948
|
+
);
|
|
4949
|
+
}
|
|
4950
|
+
const pkgSpec = `${PACKAGE_NAME}@${tag}`;
|
|
4951
|
+
const install = buildInstallCommand(pm, pkgSpec);
|
|
4952
|
+
if (options.dryRun) {
|
|
4953
|
+
const payload = {
|
|
4954
|
+
action: "upgrade",
|
|
4955
|
+
package: PACKAGE_NAME,
|
|
4956
|
+
tag,
|
|
4957
|
+
packageManager: pm,
|
|
4958
|
+
command: install.preview,
|
|
4959
|
+
dryRun: true
|
|
4960
|
+
};
|
|
4961
|
+
if (format === "json") {
|
|
4962
|
+
output(payload);
|
|
4963
|
+
} else {
|
|
4964
|
+
logger.info(`Upgrade command: ${install.preview}`);
|
|
4965
|
+
}
|
|
4966
|
+
return;
|
|
4967
|
+
}
|
|
4968
|
+
if (!options.yes) {
|
|
4969
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
4970
|
+
throw new CLIError(
|
|
4971
|
+
"Upgrade requires confirmation. Re-run with --yes to skip the prompt.",
|
|
4972
|
+
3 /* ConfigError */
|
|
4973
|
+
);
|
|
4974
|
+
}
|
|
4975
|
+
const confirmed = await confirmPrompt(`Upgrade Supercheck CLI using ${pm}?`, { default: false });
|
|
4976
|
+
if (!confirmed) {
|
|
4977
|
+
logger.info("Upgrade cancelled.");
|
|
4978
|
+
return;
|
|
4979
|
+
}
|
|
4980
|
+
}
|
|
4981
|
+
let code = 0;
|
|
4982
|
+
try {
|
|
4983
|
+
code = await withSpinner(
|
|
4984
|
+
"Upgrading Supercheck CLI...",
|
|
4985
|
+
() => runInstallCommand(install.command, install.args)
|
|
4986
|
+
);
|
|
4987
|
+
} catch (err) {
|
|
4988
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4989
|
+
throw new CLIError(`Failed to run upgrade command: ${message}`, 1 /* GeneralError */);
|
|
4990
|
+
}
|
|
4991
|
+
if (code !== 0) {
|
|
4992
|
+
throw new CLIError(`Upgrade failed with exit code ${code}.`, 1 /* GeneralError */);
|
|
4993
|
+
}
|
|
4994
|
+
if (format === "json") {
|
|
4995
|
+
output({
|
|
4996
|
+
action: "upgrade",
|
|
4997
|
+
package: PACKAGE_NAME,
|
|
4998
|
+
tag,
|
|
4999
|
+
packageManager: pm,
|
|
5000
|
+
command: install.preview,
|
|
5001
|
+
success: true
|
|
5002
|
+
});
|
|
5003
|
+
return;
|
|
5004
|
+
}
|
|
5005
|
+
logger.success(`Supercheck CLI upgraded (${pkgSpec}).`);
|
|
5006
|
+
});
|
|
5007
|
+
|
|
4020
5008
|
// src/bin/supercheck.ts
|
|
4021
|
-
var program = new
|
|
5009
|
+
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
5010
|
const opts = program.opts();
|
|
4023
5011
|
if (opts.json) {
|
|
4024
5012
|
setOutputFormat("json");
|
|
@@ -4038,13 +5026,14 @@ program.addCommand(whoamiCommand);
|
|
|
4038
5026
|
program.addCommand(initCommand);
|
|
4039
5027
|
program.addCommand(configCommand);
|
|
4040
5028
|
program.addCommand(jobCommand);
|
|
4041
|
-
program.addCommand(
|
|
5029
|
+
program.addCommand(runCommand2);
|
|
4042
5030
|
program.addCommand(testCommand);
|
|
4043
5031
|
program.addCommand(monitorCommand);
|
|
4044
5032
|
program.addCommand(varCommand);
|
|
4045
5033
|
program.addCommand(tagCommand);
|
|
4046
5034
|
program.addCommand(diffCommand);
|
|
4047
5035
|
program.addCommand(deployCommand);
|
|
5036
|
+
program.addCommand(validateCommand);
|
|
4048
5037
|
program.addCommand(destroyCommand);
|
|
4049
5038
|
program.addCommand(pullCommand);
|
|
4050
5039
|
program.addCommand(notificationCommand);
|
|
@@ -4052,14 +5041,42 @@ program.addCommand(alertCommand);
|
|
|
4052
5041
|
program.addCommand(auditCommand);
|
|
4053
5042
|
program.addCommand(healthCommand);
|
|
4054
5043
|
program.addCommand(locationsCommand);
|
|
5044
|
+
program.addCommand(doctorCommand);
|
|
5045
|
+
program.addCommand(upgradeCommand);
|
|
4055
5046
|
program.exitOverride();
|
|
4056
5047
|
async function main() {
|
|
4057
5048
|
try {
|
|
4058
5049
|
await program.parseAsync(process.argv);
|
|
4059
5050
|
} catch (err) {
|
|
5051
|
+
if (err instanceof ApiRequestError && err.responseBody !== void 0) {
|
|
5052
|
+
const details = (() => {
|
|
5053
|
+
if (typeof err.responseBody === "string") {
|
|
5054
|
+
return err.responseBody.trim() ? `Details: ${err.responseBody}` : "";
|
|
5055
|
+
}
|
|
5056
|
+
if (err.responseBody && typeof err.responseBody === "object") {
|
|
5057
|
+
return `Details: ${JSON.stringify(err.responseBody, null, 2)}`;
|
|
5058
|
+
}
|
|
5059
|
+
return err.responseBody ? `Details: ${String(err.responseBody)}` : "";
|
|
5060
|
+
})();
|
|
5061
|
+
const suffix = details ? `
|
|
5062
|
+
${details}
|
|
5063
|
+
` : "\n";
|
|
5064
|
+
console.error(pc10.red(`
|
|
5065
|
+
\u2717 ${err.message}${suffix}`));
|
|
5066
|
+
process.exit(err.exitCode);
|
|
5067
|
+
}
|
|
5068
|
+
if (err instanceof DependencyError) {
|
|
5069
|
+
console.error(pc10.red(`
|
|
5070
|
+
\u2717 Missing dependency: ${err.dependency}`));
|
|
5071
|
+
console.error(pc10.yellow(`
|
|
5072
|
+
${err.message}
|
|
5073
|
+
`));
|
|
5074
|
+
console.error(pc10.dim("Run `supercheck doctor` for a full dependency check.\n"));
|
|
5075
|
+
process.exit(3 /* ConfigError */);
|
|
5076
|
+
}
|
|
4060
5077
|
if (err instanceof CLIError) {
|
|
4061
5078
|
if (err.exitCode !== 0 /* Success */) {
|
|
4062
|
-
console.error(
|
|
5079
|
+
console.error(pc10.red(`
|
|
4063
5080
|
\u2717 ${err.message}
|
|
4064
5081
|
`));
|
|
4065
5082
|
}
|
|
@@ -4068,7 +5085,7 @@ async function main() {
|
|
|
4068
5085
|
if (err && typeof err === "object" && "exitCode" in err && typeof err.exitCode === "number") {
|
|
4069
5086
|
process.exit(err.exitCode);
|
|
4070
5087
|
}
|
|
4071
|
-
console.error(
|
|
5088
|
+
console.error(pc10.red(`
|
|
4072
5089
|
\u2717 Unexpected error: ${err instanceof Error ? err.message : String(err)}
|
|
4073
5090
|
`));
|
|
4074
5091
|
process.exit(1 /* GeneralError */);
|