@supercheck/cli 0.1.0-beta.9 → 0.1.1-beta.2

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