@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.
@@ -4,8 +4,8 @@ import {
4
4
  } from "../chunk-W54DX2I7.js";
5
5
 
6
6
  // src/bin/supercheck.ts
7
- import { Command as Command18 } from "commander";
8
- import pc7 from "picocolors";
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.0-beta.9" : "0.0.0-dev";
219
+ var CLI_VERSION = true ? "0.1.1-beta.1" : "0.0.0-dev";
217
220
 
218
- // src/api/client.ts
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((resolve6) => setTimeout(resolve6, ms));
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, "{{GLOBSTAR}}").replace(/\*/g, "{{STAR}}");
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(/\{\{GLOBSTAR\}\}/g, ".*").replace(/\{\{STAR\}\}/g, "[^/]*");
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: { ...job }
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 stem = file.filename.replace(/\.(pw|k6)\.ts$/, "");
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: file.filename,
673
+ name: displayName,
608
674
  definition: {
609
675
  id: testId,
610
- title: stem,
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
- const testType = t.type === "performance" || t.type === "k6" ? "k6" : "pw";
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_/tests/**/*.pw.ts"),
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_/tests/**/*.k6.ts")
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 existsSync3, mkdirSync, writeFileSync as writeFileSync2, readFileSync as readFileSync2, appendFileSync } from "fs";
1498
- import { resolve as resolve4 } from "path";
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
- execSync(cmd, {
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_/tests/**/*.pw.ts',
1866
+ testMatch: '_supercheck_/playwright/**/*.pw.ts',
1591
1867
  browser: 'chromium',
1592
1868
  },
1593
1869
  k6: {
1594
- testMatch: '_supercheck_/tests/**/*.k6.ts',
1870
+ testMatch: '_supercheck_/k6/**/*.k6.ts',
1595
1871
  },
1596
1872
  },
1597
1873
  })
1598
1874
  `;
1599
- var EXAMPLE_PW_TEST = `import { test, expect } from '@playwright/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 = `import http from 'k6/http'
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 = resolve4(cwd, "supercheck.config.ts");
1652
- if (existsSync3(configPath) && !options.force) {
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 = resolve4(cwd, "tsconfig.supercheck.json");
1664
- if (!existsSync3(tsconfigPath) || options.force) {
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 dirs = [
1669
- "_supercheck_/tests",
1670
- "_supercheck_/monitors",
1671
- "_supercheck_/status-pages"
1672
- ];
1673
- for (const dir of dirs) {
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 pwTestPath = resolve4(cwd, "_supercheck_/tests/homepage.pw.ts");
1688
- if (!existsSync3(pwTestPath)) {
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("Created _supercheck_/tests/homepage.pw.ts (Playwright example)");
1691
- }
1692
- const k6TestPath = resolve4(cwd, "_supercheck_/tests/load-test.k6.ts");
1693
- if (!existsSync3(k6TestPath)) {
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("Created _supercheck_/tests/load-test.k6.ts (k6 example)");
1974
+ logger.success(`Created _supercheck_/k6/${k6Filename} (k6 example)`);
1696
1975
  }
1697
1976
  }
1698
- const gitignorePath = resolve4(cwd, ".gitignore");
1699
- if (existsSync3(gitignorePath)) {
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_/tests/ (*.pw.ts for Playwright, *.k6.ts for k6)");
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/validation.ts
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
- logger.success(`Job "${options.name}" created (${data.id})`);
1866
- outputDetail(data);
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
- logger.success(`Job triggered. Run ID: ${data.runId}`);
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((resolve6) => setTimeout(resolve6, 3e3));
2474
+ await new Promise((resolve10) => setTimeout(resolve10, 3e3));
1960
2475
  const { data: runData } = await statusClient.get(
1961
- `/api/runs/${data.runId}`
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 ${data.runId} passed`);
2481
+ logger.success(`Run ${runId} passed`);
1967
2482
  } else if (status === "blocked") {
1968
- logger.error(`Run ${data.runId} blocked`);
1969
- throw new CLIError(`Run ${data.runId} blocked`, 1 /* GeneralError */);
2483
+ logger.error(`Run ${runId} blocked`);
2484
+ throw new CLIError(`Run ${runId} blocked`, 1 /* GeneralError */);
1970
2485
  } else {
1971
- logger.error(`Run ${data.runId} ${status}`);
1972
- throw new CLIError(`Run ${data.runId} ${status}`, 1 /* GeneralError */);
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
- import { ProxyAgent as ProxyAgent2 } from "undici";
1989
- var proxyAgents2 = /* @__PURE__ */ new Map();
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
- runCommand.command("get <id>").description("Get run details").action(async (id) => {
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
- runCommand.command("status <id>").description("Get run status").action(async (id) => {
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
- runCommand.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) => {
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: getProxyAgent2(proxy) } : {},
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 { ProxyAgent as ProxyAgent3 } from "undici";
2679
+ import { readFileSync as readFileSync4 } from "fs";
2680
+ import { resolve as resolve7 } from "path";
2189
2681
  import { Buffer as Buffer2 } from "buffer";
2190
- var proxyAgents3 = /* @__PURE__ */ new Map();
2191
- function getProxyAgent3(proxyUrl) {
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 isNoProxyMatch2(url, noProxyRaw) {
2199
- const hostname = url.hostname;
2200
- const port = url.port || (url.protocol === "https:" ? "443" : "80");
2201
- const entries = noProxyRaw.split(",").map((entry) => entry.trim()).filter(Boolean);
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 getProxyEnv2(url) {
2219
- const noProxyRaw = process.env.NO_PROXY ?? process.env.no_proxy;
2220
- if (noProxyRaw && isNoProxyMatch2(url, noProxyRaw)) {
2221
- return null;
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
- if (url.protocol === "https:") {
2224
- return process.env.HTTPS_PROXY ?? process.env.https_proxy ?? process.env.HTTP_PROXY ?? process.env.http_proxy ?? null;
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
- if (url.protocol === "http:") {
2227
- return process.env.HTTP_PROXY ?? process.env.http_proxy ?? null;
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 normalizeTestType(input) {
2232
- const normalized = input.trim().toLowerCase();
2233
- if (normalized === "k6") return "performance";
2234
- if (normalized === "performance") return "performance";
2235
- if (normalized === "playwright") return "browser";
2236
- if (normalized === "browser") return "browser";
2237
- return normalized;
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 (playwright, k6)").action(async (options) => {
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
- if (options.includeScript) params.includeScript = "true";
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 (playwright, k6)", "playwright").option("--description <description>", "Test description").action(async (options) => {
2281
- const { readFileSync: readFileSync4 } = await import("fs");
2282
- const { resolve: resolve6 } = await import("path");
2283
- const filePath = resolve6(process.cwd(), options.file);
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 = readFileSync4(filePath, "utf-8");
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(options.type)
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: readFileSync4 } = await import("fs");
2313
- const { resolve: resolve6 } = await import("path");
2314
- const filePath = resolve6(process.cwd(), options.file);
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 = readFileSync4(filePath, "utf-8");
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 (playwright, k6)", "playwright").action(async (options) => {
2374
- const { readFileSync: readFileSync4 } = await import("fs");
2375
- const { resolve: resolve6 } = await import("path");
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 { data } = await withSpinner(
2973
+ const results = await withSpinner(
2385
2974
  "Validating script",
2386
- () => client.post(
2387
- "/api/validate-script",
2388
- { script, testType: normalizeTestType(options.type) }
2389
- )
2975
+ () => validateScripts(client, [{
2976
+ name: toDisplayName2(cwd, filePath),
2977
+ script,
2978
+ testType: normalizeTestTypeForApi(typeArg)
2979
+ }])
2390
2980
  );
2391
- if (data.valid) {
2392
- logger.success(`Script is valid (${options.type})`);
2393
- } else {
2394
- logger.error("Script validation failed:");
2395
- if (data.errors) {
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
- throw new CLIError("Script validation failed", 1 /* GeneralError */);
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 = getProxyEnv2(parsedUrl);
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: getProxyAgent3(proxy) } : {},
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 pc3 from "picocolors";
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(pc3.green(` + ${creates.length} to create`));
3442
+ logger.info(pc4.green(` + ${creates.length} to create`));
2855
3443
  for (const c of creates) {
2856
- logger.info(pc3.green(` + ${c.type}/${c.name}`));
3444
+ logger.info(pc4.green(` + ${c.type}/${c.name}`));
2857
3445
  }
2858
3446
  }
2859
3447
  if (updates.length > 0) {
2860
- logger.info(pc3.yellow(` ~ ${updates.length} to update`));
3448
+ logger.info(pc4.yellow(` ~ ${updates.length} to update`));
2861
3449
  for (const c of updates) {
2862
- logger.info(pc3.yellow(` ~ ${c.type}/${c.name} (${c.id})`));
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(pc3.gray(` ${d}`));
3453
+ logger.info(pc4.gray(` ${d}`));
2866
3454
  }
2867
3455
  }
2868
3456
  }
2869
3457
  }
2870
3458
  if (deletes.length > 0) {
2871
- logger.info(pc3.red(` - ${deletes.length} to delete`));
3459
+ logger.info(pc4.red(` - ${deletes.length} to delete`));
2872
3460
  for (const c of deletes) {
2873
- logger.info(pc3.red(` - ${c.type}/${c.name} (${c.id})`));
3461
+ logger.info(pc4.red(` - ${c.type}/${c.name} (${c.id})`));
2874
3462
  }
2875
3463
  }
2876
3464
  if (unchanged.length > 0) {
2877
- logger.info(pc3.gray(` = ${unchanged.length} unchanged`));
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(`${pc3.bold(String(totalChanges))} change(s) detected.`);
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 pc4 from "picocolors";
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 body = prepareBodyForApi(change.type, change.local.definition);
2936
- await client.post(endpoint, body);
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(pc4.yellow("Dry run \u2014 no changes applied."));
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" ? pc4.green : change.action === "update" ? pc4.yellow : pc4.red;
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 pc5 from "picocolors";
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(pc5.red(` - ${r.type}/${r.name} [${r.id}]`));
3814
+ logger.info(pc6.red(` - ${r.type}/${r.name} [${r.id}]`));
3161
3815
  }
3162
3816
  logger.newline();
3163
- logger.warn(`This will permanently delete ${pc5.bold(String(managed.length))} resource(s).`);
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(pc5.yellow("Dry run \u2014 no resources destroyed."));
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(pc5.red(` - ${resource.type}/${resource.name} \u2713`));
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 existsSync4, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3, readFileSync as readFileSync3 } from "fs";
3207
- import { resolve as resolve5, dirname as dirname2 } from "path";
3208
- import pc6 from "picocolors";
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 (existsSync4(filePath)) {
3222
- const existing = readFileSync3(filePath, "utf-8");
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 (!existsSync4(dir)) {
3873
+ if (!existsSync5(dir)) {
3227
3874
  mkdirSync2(dir, { recursive: true });
3228
3875
  }
3229
- writeFileSync3(filePath, content, "utf-8");
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 testsDir = resolve5(cwd, "_supercheck_/tests");
3314
- if (!existsSync4(testsDir)) {
3315
- mkdirSync2(testsDir, { recursive: true });
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 filename = testFilename(test.id, testType);
3321
- const relPath = `_supercheck_/tests/${filename}`;
3322
- const script = decodeScript(test.script);
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
- const filePath = resolve5(cwd, relPath);
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(pc6.green(` + ${relPath}`));
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_/tests/**/*.pw.ts',`);
4168
+ parts.push(` testMatch: '_supercheck_/playwright/**/*.pw.ts',`);
3487
4169
  parts.push(" },");
3488
4170
  parts.push(" k6: {");
3489
- parts.push(` testMatch: '_supercheck_/tests/**/*.k6.ts',`);
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: https://grafana.com/docs/k6/latest/set-up/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(pc6.cyan(` Tests (${tests.length}):`));
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(pc6.cyan(` Monitors (${monitors.length}):`));
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(pc6.cyan(` Jobs (${jobs.length}):`));
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(pc6.cyan(` Variables (${variables.length}):`));
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(pc6.cyan(` Tags (${tags.length}):`));
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(pc6.cyan(` Status Pages (${statusPages.length}):`));
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(pc6.yellow("Dry run \u2014 no files written."));
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 ${pc6.bold(String(totalResources))} resources to pull.`);
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 = `_supercheck_/tests/${testFilename(t.id, testType)}`;
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 = resolve5(cwd, "supercheck.config.ts");
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(pc6.green(` + ${monitorDefs.length} monitor(s)`));
3774
- if (jobDefs.length > 0) logger.info(pc6.green(` + ${jobDefs.length} job(s)`));
3775
- if (variableDefs.length > 0) logger.info(pc6.green(` + ${variableDefs.length} variable(s)`));
3776
- if (tagDefs.length > 0) logger.info(pc6.green(` + ${tagDefs.length} tag(s)`));
3777
- if (statusPageDefs.length > 0) logger.info(pc6.green(` + ${statusPageDefs.length} status page(s)`));
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/notifications.ts
4543
+ // src/commands/validate.ts
3819
4544
  import { Command as Command15 } from "commander";
3820
- var notificationCommand = new Command15("notification").alias("notifications").description("Manage notification providers");
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 Command16 } from "commander";
3953
- var alertCommand = new Command16("alert").alias("alerts").description("View alert history");
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 Command17 } from "commander";
3982
- var auditCommand = new Command17("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) => {
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 Command18().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) => {
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(runCommand);
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(pc7.red(`
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(pc7.red(`
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 */);