UNPKG

appwarden

Version:

CLI tool to manage Appwarden monitors and deployments

818 lines (793 loc) 28.4 kB
#!/usr/bin/env node // src/index.ts import { Agent, setGlobalDispatcher } from "undici"; // src/runner/index.ts import { Command, Help } from "commander"; // src/tasks/domains-view/domains-view.ts import fs from "fs"; import YAML from "yaml"; // src/constants.ts var appwardenDir = "./.appwarden"; var domainsDir = `${appwardenDir}/domains`; var workerDir = `${appwardenDir}/cloudflare-worker`; function splitOnSerialNumber(str) { const index = str.search(/\d{4}/); if (index === -1) { return [str, ""]; } const before = str.substring(0, index); const after = str.substring(index); return [before, after]; } var unquote = (str) => ( // replace the opening and closing quotation in str if they exist str.replace(/^"|"$/g, "") ); var allowedDNSRecords = [ { type: "A" }, { type: "AAAA" }, { type: "CNAME" }, { type: "TXT", transformer: unquote }, { type: "MX" }, { type: "NS", transformer: (value) => value.endsWith(".") ? value : `${value}.` }, { type: "SOA", transformer: (value) => { const [prefix] = splitOnSerialNumber(value); return prefix.trim(); }, validator: (value) => { const [, soaNumbers] = splitOnSerialNumber(value); if (soaNumbers) { return "starts-with"; } return void 0; } }, { type: "SRV" }, { type: "CAA" }, { type: "DS" }, { type: "DNSKEY" }, { type: "RRSIG" }, { type: "NSEC" }, { type: "NSEC3" }, { type: "NSEC3PARAM" }, { type: "SPF" }, { type: "DKIM" }, { type: "DMARC" } ]; var vercelCIDRBlocks = [ { comment: "CIDR range for https://dnschecker.org/ip-whois-lookup.php?query=76.76.21.0", content: "76.76.21.0/24" }, { comment: "CIDR range for https://dnschecker.org/ip-whois-lookup.php?query=216.198.79.0", content: "216.198.79.0/24" }, { comment: "CIDR range for https://dnschecker.org/ip-whois-lookup.php?query=66.33.60.0", content: "66.33.60.0/24" }, { comment: "CIDR range for https://myip.ms/view/ip_owners/1127467/Vercel_Inc.html", content: "216.230.84.0/24" }, { comment: "CIDR range for https://myip.ms/view/ip_owners/1127467/Vercel_Inc.html", content: "198.169.2.0/24" }, { comment: "CIDR range for https://myip.ms/view/ip_owners/1127467/Vercel_Inc.html", content: "64.29.17.0/24" }, { comment: "CIDR range for https://myip.ms/view/ip_owners/1127467/Vercel_Inc.html", content: "198.169.1.0/24" }, { comment: "CIDR range for https://myip.ms/view/ip_owners/1127467/Vercel_Inc.html", content: "64.239.109.0/24" } ]; var allowedHeaderNames = [ "content-type", // Note: transfer-encoding is excluded because it's not stable across CDNs/servers // (servers dynamically choose between transfer-encoding: chunked vs content-length) "connection", "cache-control", "vary", "server", // security headers "content-security-policy", "x-content-type-options", "x-frame-options", "strict-transport-security", "x-xss-protection", "referrer-policy", "feature-policy", "permissions-policy", "cross-origin-embedder-policy", "cross-origin-opener-policy", "cross-origin-resource-policy", "access-control-allow-origin", "access-control-allow-methods", "access-control-allow-headers", "access-control-expose-headers", "access-control-allow-credentials", "access-control-max-age", "public-key-pins", "expect-ct" ]; // src/tasks/domains-view/domains-view.ts var readDomainConfig = (filePath) => { try { const yamlContent = fs.readFileSync(filePath, "utf-8"); return YAML.parse(yamlContent); } catch { return null; } }; var findMiddleware = (middleware, url) => middleware?.find((m) => m.url === url); var buildWebsiteView = (monitor, middleware) => ({ url: monitor.url, recordCount: monitor.headers?.length ?? 0, middlewareConfigured: !!middleware, cspConfigured: !!middleware?.options?.["csp-mode"] || !!middleware?.options?.["csp-directives"], lockPageSlug: middleware?.options?.["lock-page-slug"] }); var buildDomainView = (config) => { const middleware = config.services?.middleware ?? []; const monitors = config.services?.monitors ?? []; return { hostname: config.hostname, dnsRecordCount: config.dns?.monitors?.length ?? 0, services: monitors.map( (monitor) => buildWebsiteView(monitor, findMiddleware(middleware, monitor.url)) ) }; }; var getDomainViews = (hostname) => { if (!fs.existsSync(domainsDir)) { return []; } const files = fs.readdirSync(domainsDir); const views = []; for (const file of files) { if (!file.endsWith(".yml")) continue; const config = readDomainConfig(`${domainsDir}/${file}`); if (!config) continue; if (hostname && config.hostname !== hostname) continue; views.push(buildDomainView(config)); } return views; }; var buildInitCommand = (views) => { const domains = /* @__PURE__ */ new Set(); for (const view of views) { domains.add(view.hostname); for (const website of view.services) { domains.add(website.url); } } const args = Array.from(domains).filter((d) => d !== "example.com").map((d) => `-d ${d}`).join(" "); return `appwarden sync ${args}`; }; // src/commands/domains-view/domains-view.ts var printTree = (views) => { for (let i = 0; i < views.length; i++) { const view = views[i]; const isLastDomain = i === views.length - 1; const domainPrefix = isLastDomain ? "\u2514\u2500\u2500" : "\u251C\u2500\u2500"; const domainIndent = isLastDomain ? " " : "\u2502 "; console.log(`${domainPrefix} ${view.hostname}`); console.log(`${domainIndent}\u2514\u2500\u2500 DNS records: ${view.dnsRecordCount}`); for (let j = 0; j < view.services.length; j++) { const website = view.services[j]; const isLastWebsite = j === view.services.length - 1; const websitePrefix = isLastWebsite ? "\u2514\u2500\u2500" : "\u251C\u2500\u2500"; const websiteIndent = isLastWebsite ? " " : "\u2502 "; const websiteConnector = " "; console.log(`${domainIndent}${websiteConnector}${websitePrefix} ${website.url}`); console.log( `${domainIndent}${websiteConnector}${websiteIndent}\u251C\u2500\u2500 Records: ${website.recordCount}` ); console.log( `${domainIndent}${websiteConnector}${websiteIndent}\u2514\u2500\u2500 Middleware: ${website.middlewareConfigured ? "yes" : "no"}` ); if (website.middlewareConfigured) { console.log( `${domainIndent}${websiteConnector}${websiteIndent} \u251C\u2500\u2500 CSP: ${website.cspConfigured ? "yes" : "no"}` ); console.log( `${domainIndent}${websiteConnector}${websiteIndent} \u2514\u2500\u2500 Lock page: ${website.lockPageSlug ?? "none"}` ); } } } }; var domainsViewCommand = { command: "domains view [hostname]", description: "View domain configuration files in .appwarden/domains", options: ["--json", "Output the result as JSON"], action: async (args) => { const views = getDomainViews(args.hostname); if (views.length === 0) { console.log("[@appwarden] No domain configurations found in .appwarden/domains"); return; } if (args.json) { console.log(JSON.stringify(views, null, 2)); } else { printTree(views); } console.log(""); console.log( `[@appwarden] This a summary of your domain configuration files in this repository. To view your deployed configuration, visit the Appwarden [dashboard](https://use.appwarden.io)` ); console.log( `[@appwarden] Run the following command to sync your domain configurations with detected records: ${buildInitCommand(views)} ` ); } }; // src/commands/sync/sync.ts import fs2 from "fs"; import YAML3 from "yaml"; // src/utils/debug.ts var debugEnabled = false; var setDebug = (enabled) => { debugEnabled = enabled; }; var debug = (...msg) => { if (debugEnabled) { console.log(...msg); } }; // src/utils/ensure-protocol.ts var protocolRegex = /^https?:\/\//i; var ensureProtocol = (maybeFQDN) => { const hasProtocol = protocolRegex.test(maybeFQDN); if (!hasProtocol) { return `https://${maybeFQDN}`; } return maybeFQDN; }; // src/utils/get-dns-records.ts import { getAllDnsRecords } from "@layered/dns-records"; // src/utils/parse-domain.ts import { ParseResultType, fromUrl, parseDomain } from "parse-domain"; var parseHostname = (hostname) => parseDomain(fromUrl(hostname)); var getDomainMetadata = (thing) => { const parseResult = parseHostname(thing); if (parseResult.type === ParseResultType.Listed && parseResult.domain) { return parseResult; } return false; }; var getRootDomain = (listed) => listed.domain + "." + listed.topLevelDomains.join("."); var transformLayeredDNSRecords = (recordsList) => sortDNSRecords( recordsList.map((record) => ({ name: record.name, type: record.type, content: record.data })) ); var sortDNSRecords = (records) => records.sort((a, b) => { if (a.type && b.type) { if (a.type < b.type) { return -1; } if (a.type > b.type) { return 1; } } if (a.name && b.name) { if (a.name < b.name) { return -1; } if (a.name > b.name) { return 1; } } if (a.content && b.content) { if (a.content < b.content) { return -1; } if (a.content > b.content) { return 1; } } return 0; }); // src/utils/get-dns-records.ts var getDNSRecords = (domain) => getAllDnsRecords(domain, { resolver: "cloudflare-dns", commonSubdomainsCheck: false, subdomains: [] }).then((records) => transformLayeredDNSRecords(records)); // src/utils/is-cloudflare.ts var isCloudflareResponse = (response) => { const server = response.headers.get("server"); const cfRay = response.headers.get("cf-ray"); return server?.toLowerCase() === "cloudflare" || cfRay !== null && cfRay.length > 0; }; // src/utils/normalize-csp.ts var normalizeCsp = (headerValue) => { return headerValue.replaceAll(/'nonce-[^']+'/g, "{{nonce}}"); }; // src/utils/stringify-yaml-with-comments.ts import YAML2 from "yaml"; function stringifyYAMLWithComments(content) { const cleanContent = removeCommentProperties(content); const doc = YAML2.parseDocument(YAML2.stringify(cleanContent)); if (doc.contents && YAML2.isMap(doc.contents)) { const dnsNode = doc.contents.get("dns"); if (dnsNode && YAML2.isMap(dnsNode)) { const monitorsNode = dnsNode.get("monitors"); if (monitorsNode && YAML2.isSeq(monitorsNode)) { monitorsNode.items.forEach((monitorItem, monitorIndex) => { if (!YAML2.isMap(monitorItem)) return; const originalMonitor = content.dns?.monitors?.[monitorIndex]; if (!originalMonitor) return; if (originalMonitor.comment && YAML2.isNode(monitorItem)) { monitorItem.commentBefore = ` ${originalMonitor.comment}`; } const contentNode = monitorItem.get("content"); if (contentNode && YAML2.isSeq(contentNode) && Array.isArray(originalMonitor.content)) { contentNode.items.forEach((contentItem, contentIndex) => { const originalContent = originalMonitor.content[contentIndex]; if (originalContent && typeof originalContent === "object" && originalContent.comment && YAML2.isNode(contentItem)) { contentItem.commentBefore = ` ${originalContent.comment}`; } }); } }); } } } return doc.toString(); } function removeCommentProperties(obj) { if (Array.isArray(obj)) { return obj.map((item) => removeCommentProperties(item)); } else if (obj && typeof obj === "object") { const result = {}; for (const [key, value] of Object.entries(obj)) { if (key !== "comment") { result[key] = removeCommentProperties(value); } } return result; } return obj; } // src/utils/is-vercel-ip.ts import ipaddr from "ipaddr.js"; var isIpInCIDRBlock = (ip, cidr) => { try { const addr = ipaddr.parse(ip); const range = ipaddr.parseCIDR(cidr); return addr.kind() === range[0].kind() && addr.match(range); } catch (_error) { return false; } }; var isVercelIP = (ip) => { return vercelCIDRBlocks.some((block) => isIpInCIDRBlock(ip, block.content)); }; var hasVercelIPs = (ips) => { return ips.some((ip) => isVercelIP(ip)); }; // src/utils/sleep.ts var sleep = (timeout = 1e3) => new Promise((resolve) => setTimeout(resolve, timeout)); // src/commands/sync/sync.ts var YAML_LANGUAGE_SERVER_SCHEMA_COMMENT = "# yaml-language-server: $schema=../../node_modules/appwarden/build/domain-configuration-file.json"; var readExistingConfig = (domain) => { const filePath = `${domainsDir}/${domain.replaceAll(".", "-")}.yml`; if (!fs2.existsSync(filePath)) { return null; } try { const yamlContent = fs2.readFileSync(filePath, "utf-8"); const parsed = YAML3.parse(yamlContent); return parsed; } catch (error) { console.warn(`[@appwarden] Warning: Could not parse existing config for ${domain}:`, error); return null; } }; var logFinishedScaffolding = (domains) => { console.log( `[@appwarden] Created an .appwarden folder in your current directory with ${domains.length} ${domains.length === 1 ? "domain" : "domains"}...` ); if (domains.length === 0) { console.log( "[@appwarden] To monitor a domain with Appwarden, run `npx appwarden sync -d example.com`." ); } const exampleFilePath = `${domainsDir}/example-com.yml`; const exampleFileExisted = fs2.existsSync(exampleFilePath); if (exampleFileExisted) { fs2.unlinkSync(exampleFilePath); } console.log(`[@appwarden] .appwarden/`); console.log(`[@appwarden] \u2514\u2500\u2500 domains/`); if (fs2.existsSync(".appwarden/domains")) { fs2.readdirSync(".appwarden/domains").forEach((file, i, list) => { const isLast = i === list.length - 1; const prefix = isLast ? "\u2514\u2500\u2500" : "\u251C\u2500\u2500"; console.log(`[@appwarden] ${prefix} ${file}`); }); } if (exampleFileExisted) { console.log( "[@appwarden] Deleted `.appwarden/domains/example-com.yml` because it is only used for tutorial purposes and is not needed for production use." ); } console.log(`[@appwarden]`); }; var syncCommand = { command: "sync", description: "Scaffold .appwarden directories and sync domain configuration files with live DNS records and website headers for the provided domains. Extends existing configurations without overwriting them.", options: [ "-d, --domain <domains...>", "Create a domain configuration file for this domain (e.g. appwarden.io)", "--vercel", "Manually enable Vercel-specific DNS monitoring with CIDR block validation (auto-detected by default)" ], action: async (args) => { console.log("[@appwarden] Running `sync` command..."); const domains = args.domain ?? []; const domainsMap = {}; for (const domain of domains) { const domainMetadata = getDomainMetadata(domain); if (!domainMetadata) { console.error(`[@appwarden] Invalid domain: ${domain}`); throw new Error(`Invalid domain: ${domain}`); } const rootDomain = getRootDomain(domainMetadata); if (rootDomain) { domainsMap[rootDomain] = [...domainsMap[rootDomain] ?? [], domainMetadata]; } } try { for (const dir of [appwardenDir, domainsDir]) { fs2.mkdirSync(dir, { recursive: true }); } } catch (_error) { } if (Object.keys(domainsMap).length === 0) { return logFinishedScaffolding(domains); } for (const [domain, subdomains] of Object.entries(domainsMap)) { const existingConfig = readExistingConfig(domain); const subdomainsBeingProcessed = new Set(subdomains.map((s) => s.hostname)); let monitorFileContent = { hostname: domain, version: 1, dns: { options: existingConfig?.dns?.options ?? { links: [] }, monitors: existingConfig?.dns?.monitors ?? [] }, // Ensure services.options appears first under `services` in the generated YAML services: existingConfig?.services ?? { options: { links: [] }, monitors: [], middleware: [] } }; let dnsRecords = []; try { const subdomainsList = subdomains.map((subdomain) => subdomain.hostname).flat(); const promises = []; for (const subdomain of subdomainsList) { promises.push(getDNSRecords(subdomain)); } dnsRecords = (await Promise.all(promises)).flat(); } catch (error) { if (error instanceof Error) { console.error(`[@appwarden] Error fetching DNS records for ${domain}:`, error.message); } } debug(`Detected DNS Records:`, dnsRecords); let isFirstRecord = true; const monitoredRecords = []; const detectedARecords = dnsRecords.filter((record) => record.type === "A"); const detectedARecordIPs = detectedARecords.map((record) => record.content); const isVercelDetected = hasVercelIPs(detectedARecordIPs); const shouldUseVercelCIDR = args.vercel || isVercelDetected; if (shouldUseVercelCIDR) { if (isVercelDetected && !args.vercel) { console.log( "[@appwarden] Detected Vercel infrastructure - automatically enabling CIDR block validation for dynamic IPs..." ); } else if (args.vercel) { console.log( "[@appwarden] Adding Vercel-specific dynamic IP monitoring with CIDR block validation..." ); } monitoredRecords.push({ name: domain, type: "A", validator: "cidr-block", comment: "Vercel uses dynamic IP addresses for traffic optimization.", content: vercelCIDRBlocks }); } for (const dnsRecord of dnsRecords) { for (const allowedRecord of allowedDNSRecords) { if (shouldUseVercelCIDR && dnsRecord.type === "A") { continue; } if (dnsRecord.type === "TXT" && typeof dnsRecord.content === "string" && dnsRecord.content.startsWith("appwarden-domain-verification")) { continue; } if (dnsRecord.type !== allowedRecord.type) { continue; } const existingRecord = monitoredRecords.find( (record) => record.name === dnsRecord.name && record.type === dnsRecord.type ); const makeDNSEntry = (dnsRecord2, allowedRecord2, isFirstRecord2) => ({ content: allowedRecord2.transformer ? allowedRecord2.transformer(dnsRecord2.content) : dnsRecord2.content, ...allowedRecord2.validator && { validator: allowedRecord2.validator(dnsRecord2.content) }, ...isFirstRecord2 && { comment: "This is the first nested record" } }); if (existingRecord) { if (typeof existingRecord.content === "string") { existingRecord.content = [ makeDNSEntry(existingRecord, allowedRecord, isFirstRecord), makeDNSEntry(dnsRecord, allowedRecord) ]; isFirstRecord = false; } else { existingRecord.content.push(makeDNSEntry(dnsRecord, allowedRecord)); } } else { monitoredRecords.push({ name: dnsRecord.name, type: dnsRecord.type, ...makeDNSEntry(dnsRecord, allowedRecord) }); } } } debug("Monitoring DNS Records:", JSON.stringify(monitoredRecords, null, 2)); const existingDNSMonitors = existingConfig?.dns?.monitors ?? []; const preservedDNSMonitors = existingDNSMonitors.filter( (monitor) => !subdomainsBeingProcessed.has(monitor.name) ); monitorFileContent.dns.monitors = [...preservedDNSMonitors, ...monitoredRecords]; const newWebsiteMonitors = []; const cloudflareHostedUrls = /* @__PURE__ */ new Set(); for (const subdomain of subdomains) { const subdomainWebsiteMonitor = { url: subdomain.hostname, status: 200, headers: [] }; let websiteResponse = null; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 1e4); try { websiteResponse = await fetch(new URL(ensureProtocol(subdomain.hostname)), { headers: { "content-type": "text/html; charset=utf-8" }, signal: controller.signal }); } catch (error) { if (error instanceof Error) { console.log( `[@appwarden] Error fetching website headers from ${subdomain.hostname}:`, error.message ); } } finally { clearTimeout(timeoutId); } const websiteHeaders = []; if (websiteResponse) { for (const headerName of allowedHeaderNames) { let headerValue = websiteResponse.headers.get(headerName); if (headerValue) { if (headerName === "content-security-policy") { headerValue = normalizeCsp(headerValue); } websiteHeaders.push({ name: headerName, value: headerValue }); } } if (isCloudflareResponse(websiteResponse)) { cloudflareHostedUrls.add(subdomain.hostname); } } subdomainWebsiteMonitor.headers = websiteHeaders; newWebsiteMonitors.push(subdomainWebsiteMonitor); } const existingWebsiteMonitors = existingConfig?.services?.monitors ?? []; const preservedWebsiteMonitors = existingWebsiteMonitors.filter( (monitor) => !subdomainsBeingProcessed.has(monitor.url) ); const existingMiddleware = existingConfig?.services?.middleware ?? []; const existingMiddlewareUrls = new Set(existingMiddleware.map((entry) => entry.url)); const mergedMiddleware = [...existingMiddleware]; for (const monitor of newWebsiteMonitors) { if (existingMiddlewareUrls.has(monitor.url)) { continue; } const isCloudflare = cloudflareHostedUrls.has(monitor.url); const scriptSrc = ["self"]; if (isCloudflare) { scriptSrc.push("{{nonce}}"); } mergedMiddleware.push({ url: monitor.url, options: { "lock-page-slug": "/maintenance", "csp-mode": "report-only", "csp-directives": { "script-src": scriptSrc } } }); } monitorFileContent.services = { options: existingConfig?.services?.options ?? { links: [] }, monitors: [...preservedWebsiteMonitors, ...newWebsiteMonitors], middleware: mergedMiddleware }; const yamlBody = stringifyYAMLWithComments(monitorFileContent); const yamlContent = `${YAML_LANGUAGE_SERVER_SCHEMA_COMMENT} ${yamlBody} `; debug(`${domainsDir}/${domain.replaceAll(".", "-")}.yml `, `${yamlContent} `); fs2.writeFileSync(`${domainsDir}/${domain.replaceAll(".", "-")}.yml`, yamlContent); } logFinishedScaffolding(domains); await sleep(); console.log( `[@appwarden] To finish setting up Appwarden, return to the [dashboard](https://use.appwarden.io).` ); await sleep(600); console.log( `[@appwarden] Create a pull request to your main branch and follow the instructions from the Appwarden bot.` ); await sleep(600); console.log(`[@appwarden] Learn more at https://appwarden.io/docs/getting-started.`); await sleep(); console.log(`[@appwarden] Thanks for using Appwarden, goodbye!`); } }; // src/commands/index.ts var cli = { name: "appwarden", description: "CLI tool to manage Appwarden configuration files and deployments. Learn more at https://appwarden.io/docs." }; // src/version.ts import { createRequire } from "module"; var require2 = createRequire(import.meta.url); var { version } = require2("../package.json"); var CLI_VERSION = version; // src/runner/index.ts var registerCommand = (program, cmd) => { const commandInstance = program.command(cmd.command).description(cmd.description); for (let i = 0; i < cmd.options.length; i += 2) { commandInstance.option(cmd.options[i], cmd.options[i + 1]); } commandInstance.action(cmd.action); }; var ansiColors = { cyan: "\x1B[36m", blue: "\x1B[34m", reset: "\x1B[0m" }; var isColorDisabled = () => process.env.NO_COLOR !== void 0; var colorize = (color, text) => { if (isColorDisabled()) { return text; } return `${ansiColors[color]}${text}${ansiColors.reset}`; }; var palette = { title: "cyan", commandText: "blue" }; var AppwardenHelp = class extends Help { styleTitle = (str) => colorize(palette.title, str); styleCommandText = (str) => colorize(palette.commandText, str); styleCommandDescription = (str) => str; formatHelp = (cmd, helper) => { const termWidth = helper.padWidth(cmd, helper); const helpWidth = helper.helpWidth || 80; const itemIndentWidth = 2; const itemSeparatorWidth = 2; const formatItem = (term, description) => { if (!description) { return this.styleCommandText(term); } const paddedPlainTerm = term.padEnd(termWidth + itemSeparatorWidth); const fullText = `${paddedPlainTerm}${this.styleCommandDescription(description)}`; const wrapped = helper.wrap( fullText, helpWidth - itemIndentWidth, termWidth + itemSeparatorWidth ); const lines = wrapped.split("\n"); const coloredTerm = this.styleCommandText(term); lines[0] = coloredTerm + lines[0].slice(term.length); return lines.join("\n"); }; const formatList = (textArray) => { return textArray.join("\n").replace(/^/gm, " ".repeat(itemIndentWidth)); }; const output = []; output.push(this.styleTitle(`Usage: ${helper.commandUsage(cmd)}`), ""); const commandDescription = helper.commandDescription(cmd); if (commandDescription.length > 0) { output.push(helper.wrap(this.styleCommandDescription(commandDescription), helpWidth, 0), ""); } const argumentList = helper.visibleArguments(cmd).map((argument) => { return formatItem(helper.argumentTerm(argument), helper.argumentDescription(argument)); }); if (argumentList.length > 0) { output.push(this.styleTitle("Arguments:"), formatList(argumentList), ""); } const optionList = helper.visibleOptions(cmd).map((option) => { return formatItem(helper.optionTerm(option), helper.optionDescription(option)); }); if (optionList.length > 0) { output.push(this.styleTitle("Options:"), formatList(optionList), ""); } if (helper.showGlobalOptions) { const globalOptionList = helper.visibleGlobalOptions(cmd).map((option) => { return formatItem(helper.optionTerm(option), helper.optionDescription(option)); }); if (globalOptionList.length > 0) { output.push(this.styleTitle("Global Options:"), formatList(globalOptionList), ""); } } const commandList = helper.visibleCommands(cmd).map((subCmd) => { return formatItem(helper.subcommandTerm(subCmd), helper.subcommandDescription(subCmd)); }); if (commandList.length > 0) { output.push(this.styleTitle("Commands:"), formatList(commandList), ""); } return output.join("\n"); }; }; async function runner() { const program = new Command().name(cli.name).description(cli.description).version(CLI_VERSION).option("--debug", "Enable debug output").hook("preAction", (thisCommand) => { setDebug(!!thisCommand.opts().debug); }); program.configureHelp(new AppwardenHelp()); registerCommand(program, syncCommand); registerCommand(program, domainsViewCommand); program.parse(process.argv); } // src/index.ts setGlobalDispatcher( new Agent({ allowH2: true, // Enable HTTP/2 over TLS (via ALPN negotiation) connections: 1 // Use single multiplexed connection (required for HTTP/2 with Agent) }) ); import("dotenv/config"); var run = async function() { await runner(); }; run();