From c1da2a27e709e971a3304ee1777cc73fa60ea3af Mon Sep 17 00:00:00 2001 From: juligasa <11684004+juligasa@users.noreply.github.com> Date: Sat, 14 Feb 2026 01:50:38 +0100 Subject: [PATCH 1/5] feat(ops): add deployment wizard for self-hosted Seed nodes Replace website_deployment.sh with a Bun/TypeScript deployment system: - Interactive setup wizard with environment presets (prod/staging/dev) that derive testnet and release channel automatically - Migration wizard that detects old installations, imports secrets, and fixes file ownership from the old UID 1001 container setup - Headless deployment engine with SHA-based change detection, health checks, and automatic rollback on failure - Minimal POSIX shell bootstrap (deploy.sh) for initial setup - CI workflow to verify the committed dist/deploy.js bundle stays fresh - Web container now runs as host user via docker compose user: directive, eliminating the need for sudo chown during deployment - Cron setup happens before first deploy so auto-updates survive failures --- .github/workflows/deploy-script.yml | 51 + ops/.gitignore | 2 + ops/deploy.sh | 65 ++ ops/deploy.test.ts | 826 ++++++++++++++ ops/deploy.ts | 1046 ++++++++++++++++++ ops/dist/deploy.js | 79 ++ docker-compose.yml => ops/docker-compose.yml | 1 + ops/package.json | 15 + ops/tsconfig.json | 18 + website_deployment.sh | 20 +- 10 files changed, 2122 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/deploy-script.yml create mode 100644 ops/.gitignore create mode 100755 ops/deploy.sh create mode 100644 ops/deploy.test.ts create mode 100644 ops/deploy.ts create mode 100755 ops/dist/deploy.js rename docker-compose.yml => ops/docker-compose.yml (99%) create mode 100644 ops/package.json create mode 100644 ops/tsconfig.json diff --git a/.github/workflows/deploy-script.yml b/.github/workflows/deploy-script.yml new file mode 100644 index 000000000..14b7f9462 --- /dev/null +++ b/.github/workflows/deploy-script.yml @@ -0,0 +1,51 @@ +name: Deploy Script - Check + +on: + workflow_dispatch: + push: + branches: [main] + paths: + - "ops/**" + pull_request: + paths: + - "ops/**" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + check: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install + working-directory: ops + + - name: Typecheck + run: bun run typecheck + working-directory: ops + + - name: Test + run: bun test + working-directory: ops + + - name: Build bundle + run: bun run build + working-directory: ops + + - name: Check bundle is up to date + run: | + if ! git diff --quiet ops/dist/deploy.js; then + echo "::error::ops/dist/deploy.js is out of date. Run 'bun run build' in ops/ and commit the result." + git diff --stat ops/dist/deploy.js + exit 1 + fi diff --git a/ops/.gitignore b/ops/.gitignore new file mode 100644 index 000000000..d77474afc --- /dev/null +++ b/ops/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +bun.lock diff --git a/ops/deploy.sh b/ops/deploy.sh new file mode 100755 index 000000000..829213437 --- /dev/null +++ b/ops/deploy.sh @@ -0,0 +1,65 @@ +#!/bin/sh +# Seed Node Deployment Bootstrap +# +# Downloads the bundled deployment script and runs it with Bun. +# Installs Docker and Bun only if they are not already present. +# +# Usage: +# sh <(curl -fsSL https://raw.githubusercontent.com/seed-hypermedia/seed/main/ops/deploy.sh) + +set -e + +SEED_DIR="${SEED_DIR:-/opt/seed}" +SEED_REPO_URL="${SEED_REPO_URL:-https://raw.githubusercontent.com/seed-hypermedia/seed/main}" + +command_exists() { + command -v "$@" > /dev/null 2>&1 +} + +info() { + echo "===> $*" +} + +ensure_dir() { + if [ ! -d "$1" ]; then + if [ -w "$(dirname "$1")" ]; then + mkdir -p "$1" + else + info "Creating $1 (requires sudo)" + sudo mkdir -p "$1" + sudo chown "$(id -u):$(id -g)" "$1" + fi + fi +} + +if ! command_exists docker; then + info "Installing Docker (requires sudo)..." + curl -fsSL https://get.docker.com -o /tmp/install-docker.sh + sudo sh /tmp/install-docker.sh + rm -f /tmp/install-docker.sh + info "Docker installed." +else + info "Docker already installed: $(docker --version)" +fi + +if ! command_exists bun; then + info "Installing Bun..." + curl -fsSL https://bun.sh/install | bash + export BUN_INSTALL="${HOME}/.bun" + export PATH="${BUN_INSTALL}/bin:${PATH}" + if ! command_exists bun; then + echo "ERROR: Bun installation failed. Please install manually: https://bun.sh" >&2 + exit 1 + fi + info "Bun installed: $(bun --version)" +else + info "Bun already installed: $(bun --version)" +fi + +ensure_dir "${SEED_DIR}" + +info "Downloading deployment script..." +curl -fsSL "${SEED_REPO_URL}/ops/dist/deploy.js" -o "${SEED_DIR}/deploy.js" + +info "Running deployment script..." +exec bun "${SEED_DIR}/deploy.js" diff --git a/ops/deploy.test.ts b/ops/deploy.test.ts new file mode 100644 index 000000000..f3a9460ed --- /dev/null +++ b/ops/deploy.test.ts @@ -0,0 +1,826 @@ +import { describe, test, expect, beforeEach, afterEach } from "bun:test"; +import { + mkdtemp, + rm, + readFile, + writeFile, + mkdir, + access, +} from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { + VERSION, + DEFAULT_COMPOSE_URL, + NOTIFY_SERVICE_HOST, + LIGHTNING_URL_MAINNET, + LIGHTNING_URL_TESTNET, + type SeedConfig, + type DeployPaths, + type ShellRunner, + makePaths, + makeShellRunner, + configExists, + readConfig, + writeConfig, + generateSecret, + parseDaemonEnv, + parseWebEnv, + parseImageTag, + extractDns, + generateCaddyfile, + sha256, + buildComposeEnv, + getWorkspaceDirs, + checkContainersHealthy, + getContainerImages, + ensureSeedDir, + environmentPresets, +} from "./deploy"; + +// --------------------------------------------------------------------------- +// Fixtures +// --------------------------------------------------------------------------- + +function makeTestConfig(overrides: Partial = {}): SeedConfig { + return { + domain: "https://node1.seed.run", + email: "ops@seed.hypermedia", + compose_url: DEFAULT_COMPOSE_URL, + compose_sha: "", + compose_envs: { LOG_LEVEL: "info" }, + environment: "prod", + release_channel: "latest", + testnet: false, + link_secret: "testSecret1", + analytics: false, + gateway: false, + last_script_run: "", + ...overrides, + }; +} + +function makeNoopShell(): ShellRunner { + return { + run(_cmd: string): string { + throw new Error("command not found"); + }, + runSafe(_cmd: string): string | null { + return null; + }, + exec(_cmd: string): Promise<{ stdout: string; stderr: string }> { + return Promise.reject(new Error("command not found")); + }, + }; +} + +function makeMockShell(responses: Record): ShellRunner { + return { + run(cmd: string): string { + for (const [pattern, response] of Object.entries(responses)) { + if (cmd.includes(pattern)) return response; + } + throw new Error(`command not mocked: ${cmd}`); + }, + runSafe(cmd: string): string | null { + try { + return this.run(cmd); + } catch { + return null; + } + }, + exec(cmd: string): Promise<{ stdout: string; stderr: string }> { + try { + return Promise.resolve({ stdout: this.run(cmd), stderr: "" }); + } catch (e) { + return Promise.reject(e); + } + }, + }; +} + +// --------------------------------------------------------------------------- +// makePaths +// --------------------------------------------------------------------------- + +describe("makePaths", () => { + test("creates paths from default seed dir", () => { + const paths = makePaths("/opt/seed"); + expect(paths.seedDir).toBe("/opt/seed"); + expect(paths.configPath).toBe("/opt/seed/config.json"); + expect(paths.composePath).toBe("/opt/seed/docker-compose.yml"); + expect(paths.deployLog).toBe("/opt/seed/deploy.log"); + }); + + test("creates paths from custom seed dir", () => { + const paths = makePaths("/tmp/test-seed"); + expect(paths.seedDir).toBe("/tmp/test-seed"); + expect(paths.configPath).toBe("/tmp/test-seed/config.json"); + expect(paths.composePath).toBe("/tmp/test-seed/docker-compose.yml"); + expect(paths.deployLog).toBe("/tmp/test-seed/deploy.log"); + }); +}); + +// --------------------------------------------------------------------------- +// extractDns +// --------------------------------------------------------------------------- + +describe("extractDns", () => { + test("strips https:// prefix", () => { + expect(extractDns("https://node1.seed.run")).toBe("node1.seed.run"); + }); + + test("strips http:// prefix", () => { + expect(extractDns("http://node1.seed.run")).toBe("node1.seed.run"); + }); + + test("strips trailing slashes", () => { + expect(extractDns("https://node1.seed.run/")).toBe("node1.seed.run"); + expect(extractDns("https://node1.seed.run///")).toBe("node1.seed.run"); + }); + + test("handles bare domain (no protocol)", () => { + expect(extractDns("node1.seed.run")).toBe("node1.seed.run"); + }); + + test("preserves port numbers", () => { + expect(extractDns("https://localhost:3000")).toBe("localhost:3000"); + }); + + test("preserves subdomains", () => { + expect(extractDns("https://deep.sub.domain.example.com")).toBe( + "deep.sub.domain.example.com", + ); + }); + + test("handles empty string", () => { + expect(extractDns("")).toBe(""); + }); + + test("handles just protocol", () => { + expect(extractDns("https://")).toBe(""); + }); +}); + +// --------------------------------------------------------------------------- +// sha256 +// --------------------------------------------------------------------------- + +describe("sha256", () => { + test("produces a 64-char hex string", () => { + const hash = sha256("hello world"); + expect(hash).toHaveLength(64); + expect(hash).toMatch(/^[0-9a-f]{64}$/); + }); + + test("known hash for 'hello world'", () => { + expect(sha256("hello world")).toBe( + "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", + ); + }); + + test("different inputs produce different hashes", () => { + expect(sha256("input A")).not.toBe(sha256("input B")); + }); + + test("same input produces same hash", () => { + expect(sha256("deterministic")).toBe(sha256("deterministic")); + }); + + test("handles empty string", () => { + expect(sha256("")).toBe( + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + ); + }); + + test("handles unicode content", () => { + const hash = sha256("hello δΈ–η•Œ 🌍"); + expect(hash).toHaveLength(64); + expect(hash).toMatch(/^[0-9a-f]{64}$/); + }); +}); + +// --------------------------------------------------------------------------- +// generateSecret +// --------------------------------------------------------------------------- + +describe("generateSecret", () => { + test("default length is 10", () => { + expect(generateSecret()).toHaveLength(10); + }); + + test("custom lengths", () => { + expect(generateSecret(5)).toHaveLength(5); + expect(generateSecret(20)).toHaveLength(20); + expect(generateSecret(1)).toHaveLength(1); + expect(generateSecret(0)).toBe(""); + }); + + test("only alphanumeric characters", () => { + for (let i = 0; i < 20; i++) { + expect(generateSecret(50)).toMatch(/^[A-Za-z0-9]*$/); + } + }); + + test("successive calls produce different values", () => { + const secrets = new Set(Array.from({ length: 20 }, () => generateSecret())); + expect(secrets.size).toBe(20); + }); +}); + +// --------------------------------------------------------------------------- +// environmentPresets +// --------------------------------------------------------------------------- + +describe("environmentPresets", () => { + test("prod uses mainnet and stable releases", () => { + const p = environmentPresets("prod"); + expect(p.testnet).toBe(false); + expect(p.release_channel).toBe("latest"); + }); + + test("staging uses mainnet with dev builds", () => { + const p = environmentPresets("staging"); + expect(p.testnet).toBe(false); + expect(p.release_channel).toBe("dev"); + }); + + test("dev uses testnet with dev builds", () => { + const p = environmentPresets("dev"); + expect(p.testnet).toBe(true); + expect(p.release_channel).toBe("dev"); + }); +}); + +// --------------------------------------------------------------------------- +// generateCaddyfile +// --------------------------------------------------------------------------- + +describe("generateCaddyfile", () => { + test("contains expected Caddy directives", () => { + const caddy = generateCaddyfile(makeTestConfig()); + expect(caddy).toContain("{$SEED_SITE_HOSTNAME}"); + expect(caddy).toContain("encode zstd gzip"); + expect(caddy).toContain("reverse_proxy /.metrics* grafana:"); + expect(caddy).toContain("reverse_proxy @ipfsget seed-daemon:"); + expect(caddy).toContain("reverse_proxy * seed-web:"); + }); + + test("contains IPFS get matcher", () => { + const caddy = generateCaddyfile(makeTestConfig()); + expect(caddy).toContain("@ipfsget"); + expect(caddy).toContain("method GET HEAD OPTIONS"); + expect(caddy).toContain("path /ipfs/*"); + }); + + test("uses env var placeholders for ports", () => { + const caddy = generateCaddyfile(makeTestConfig()); + expect(caddy).toContain("{$SEED_SITE_MONITORING_PORT:3001}"); + expect(caddy).toContain("{$HM_SITE_BACKEND_GRPCWEB_PORT:56001}"); + expect(caddy).toContain("{$SEED_SITE_LOCAL_PORT:3000}"); + }); + + test("output is consistent regardless of config values", () => { + const caddy1 = generateCaddyfile( + makeTestConfig({ domain: "https://a.com" }), + ); + const caddy2 = generateCaddyfile( + makeTestConfig({ domain: "https://b.com" }), + ); + expect(caddy1).toBe(caddy2); + }); +}); + +// --------------------------------------------------------------------------- +// parseDaemonEnv +// --------------------------------------------------------------------------- + +describe("parseDaemonEnv", () => { + test("extracts log level", () => { + const json = JSON.stringify(["SEED_LOG_LEVEL=debug", "OTHER=value"]); + const result = parseDaemonEnv(json); + expect(result.logLevel).toBe("debug"); + expect(result.testnet).toBe(false); + }); + + test("detects testnet when SEED_P2P_TESTNET_NAME has a value", () => { + const json = JSON.stringify([ + "SEED_LOG_LEVEL=info", + "SEED_P2P_TESTNET_NAME=dev", + ]); + expect(parseDaemonEnv(json).testnet).toBe(true); + }); + + test("no testnet when SEED_P2P_TESTNET_NAME is empty", () => { + const json = JSON.stringify(["SEED_P2P_TESTNET_NAME="]); + expect(parseDaemonEnv(json).testnet).toBe(false); + }); + + test("null logLevel when not present", () => { + expect( + parseDaemonEnv(JSON.stringify(["UNRELATED=foo"])).logLevel, + ).toBeNull(); + }); + + test("handles invalid JSON", () => { + const result = parseDaemonEnv("not valid json"); + expect(result.logLevel).toBeNull(); + expect(result.testnet).toBe(false); + }); + + test("handles empty array", () => { + const result = parseDaemonEnv("[]"); + expect(result.logLevel).toBeNull(); + expect(result.testnet).toBe(false); + }); + + test("handles empty string", () => { + const result = parseDaemonEnv(""); + expect(result.logLevel).toBeNull(); + expect(result.testnet).toBe(false); + }); +}); + +// --------------------------------------------------------------------------- +// parseWebEnv +// --------------------------------------------------------------------------- + +describe("parseWebEnv", () => { + test("extracts hostname", () => { + const json = JSON.stringify(["SEED_BASE_URL=https://node1.seed.run"]); + expect(parseWebEnv(json).hostname).toBe("https://node1.seed.run"); + }); + + test("detects gateway mode", () => { + expect(parseWebEnv(JSON.stringify(["SEED_IS_GATEWAY=true"])).gateway).toBe( + true, + ); + }); + + test("gateway false when value is 'false'", () => { + expect(parseWebEnv(JSON.stringify(["SEED_IS_GATEWAY=false"])).gateway).toBe( + false, + ); + }); + + test("detects traffic stats", () => { + expect( + parseWebEnv(JSON.stringify(["SEED_ENABLE_STATISTICS=true"])).trafficStats, + ).toBe(true); + }); + + test("extracts all fields together", () => { + const json = JSON.stringify([ + "SEED_BASE_URL=https://gateway.hyper.media", + "SEED_IS_GATEWAY=true", + "SEED_ENABLE_STATISTICS=true", + "OTHER=ignored", + ]); + const result = parseWebEnv(json); + expect(result.hostname).toBe("https://gateway.hyper.media"); + expect(result.gateway).toBe(true); + expect(result.trafficStats).toBe(true); + }); + + test("handles invalid JSON", () => { + const result = parseWebEnv("garbage"); + expect(result.hostname).toBeNull(); + expect(result.gateway).toBe(false); + expect(result.trafficStats).toBe(false); + }); + + test("handles empty string", () => { + const result = parseWebEnv(""); + expect(result.hostname).toBeNull(); + expect(result.gateway).toBe(false); + expect(result.trafficStats).toBe(false); + }); +}); + +// --------------------------------------------------------------------------- +// parseImageTag +// --------------------------------------------------------------------------- + +describe("parseImageTag", () => { + test("extracts tag from full image string", () => { + expect(parseImageTag("seedhypermedia/web:latest")).toBe("latest"); + expect(parseImageTag("seedhypermedia/web:dev")).toBe("dev"); + expect(parseImageTag("seedhypermedia/site:v1.2.3")).toBe("v1.2.3"); + }); + + test("returns 'latest' when no tag specified", () => { + expect(parseImageTag("seedhypermedia/web")).toBe("latest"); + }); + + test("handles registry prefix", () => { + expect(parseImageTag("docker.io/seedhypermedia/web:main")).toBe("main"); + }); + + test("handles multiple colons (registry:port/image:tag)", () => { + expect(parseImageTag("registry:5000/seedhypermedia/web:dev")).toBe("dev"); + }); + + test("handles empty string", () => { + expect(parseImageTag("")).toBe("latest"); + }); +}); + +// --------------------------------------------------------------------------- +// buildComposeEnv +// --------------------------------------------------------------------------- + +describe("buildComposeEnv", () => { + test("includes all required environment variables", () => { + const env = buildComposeEnv(makeTestConfig(), makePaths("/opt/seed")); + expect(env).toContain('SEED_SITE_HOSTNAME="https://node1.seed.run"'); + expect(env).toContain('SEED_SITE_DNS="node1.seed.run"'); + expect(env).toContain('SEED_SITE_TAG="latest"'); + expect(env).toContain('SEED_SITE_WORKSPACE="/opt/seed"'); + expect(env).toContain(`SEED_UID="${process.getuid!()}"`); + expect(env).toContain(`SEED_GID="${process.getgid!()}"`); + expect(env).toContain('SEED_LOG_LEVEL="info"'); + expect(env).toContain('SEED_IS_GATEWAY="false"'); + expect(env).toContain('SEED_ENABLE_STATISTICS="false"'); + expect(env).toContain('SEED_P2P_TESTNET_NAME=""'); + expect(env).toContain(`SEED_LIGHTNING_URL="${LIGHTNING_URL_MAINNET}"`); + expect(env).toContain(`NOTIFY_SERVICE_HOST="${NOTIFY_SERVICE_HOST}"`); + expect(env).toContain( + 'SEED_SITE_MONITORING_WORKDIR="/opt/seed/monitoring"', + ); + }); + + test("testnet flips lightning URL and testnet name", () => { + const env = buildComposeEnv(makeTestConfig({ testnet: true }), makePaths()); + expect(env).toContain(`SEED_LIGHTNING_URL="${LIGHTNING_URL_TESTNET}"`); + expect(env).toContain('SEED_P2P_TESTNET_NAME="dev"'); + }); + + test("mainnet uses mainnet lightning URL", () => { + const env = buildComposeEnv( + makeTestConfig({ testnet: false }), + makePaths(), + ); + expect(env).toContain(`SEED_LIGHTNING_URL="${LIGHTNING_URL_MAINNET}"`); + expect(env).toContain('SEED_P2P_TESTNET_NAME=""'); + }); + + test("reflects gateway flag", () => { + expect( + buildComposeEnv(makeTestConfig({ gateway: true }), makePaths()), + ).toContain('SEED_IS_GATEWAY="true"'); + expect( + buildComposeEnv(makeTestConfig({ gateway: false }), makePaths()), + ).toContain('SEED_IS_GATEWAY="false"'); + }); + + test("reflects analytics flag", () => { + expect( + buildComposeEnv(makeTestConfig({ analytics: true }), makePaths()), + ).toContain('SEED_ENABLE_STATISTICS="true"'); + expect( + buildComposeEnv(makeTestConfig({ analytics: false }), makePaths()), + ).toContain('SEED_ENABLE_STATISTICS="false"'); + }); + + test("reflects release channel", () => { + expect( + buildComposeEnv(makeTestConfig({ release_channel: "dev" }), makePaths()), + ).toContain('SEED_SITE_TAG="dev"'); + expect( + buildComposeEnv( + makeTestConfig({ release_channel: "latest" }), + makePaths(), + ), + ).toContain('SEED_SITE_TAG="latest"'); + }); + + test("reflects log level", () => { + expect( + buildComposeEnv( + makeTestConfig({ + compose_envs: { LOG_LEVEL: "debug" }, + }), + makePaths(), + ), + ).toContain('SEED_LOG_LEVEL="debug"'); + }); + + test("uses custom paths for workspace and monitoring", () => { + const env = buildComposeEnv(makeTestConfig(), makePaths("/custom/path")); + expect(env).toContain('SEED_SITE_WORKSPACE="/custom/path"'); + expect(env).toContain( + 'SEED_SITE_MONITORING_WORKDIR="/custom/path/monitoring"', + ); + }); + + test("handles domain with special characters", () => { + const env = buildComposeEnv( + makeTestConfig({ domain: "https://my-node.example.com" }), + makePaths(), + ); + expect(env).toContain('SEED_SITE_DNS="my-node.example.com"'); + }); +}); + +// --------------------------------------------------------------------------- +// getWorkspaceDirs +// --------------------------------------------------------------------------- + +describe("getWorkspaceDirs", () => { + test("includes base and monitoring directories", () => { + const dirs = getWorkspaceDirs(makePaths("/opt/seed")); + expect(dirs).toContain("/opt/seed/proxy"); + expect(dirs).toContain("/opt/seed/proxy/data"); + expect(dirs).toContain("/opt/seed/proxy/config"); + expect(dirs).toContain("/opt/seed/web"); + expect(dirs).toContain("/opt/seed/daemon"); + expect(dirs).toContain("/opt/seed/monitoring"); + expect(dirs).toContain("/opt/seed/monitoring/grafana"); + expect(dirs).toContain("/opt/seed/monitoring/prometheus"); + expect(dirs).toHaveLength(8); + }); + + test("respects custom paths", () => { + const dirs = getWorkspaceDirs(makePaths("/tmp/test-seed")); + expect(dirs.every((d) => d.startsWith("/tmp/test-seed"))).toBe(true); + }); +}); + +// --------------------------------------------------------------------------- +// Config read/write/exists (integration with temp dirs) +// --------------------------------------------------------------------------- + +describe("config read/write/exists", () => { + let tmpDir: string; + let paths: DeployPaths; + + beforeEach(async () => { + tmpDir = await mkdtemp(join(tmpdir(), "seed-test-")); + paths = makePaths(tmpDir); + }); + + afterEach(async () => { + await rm(tmpDir, { recursive: true, force: true }); + }); + + test("configExists returns false when no config", async () => { + expect(await configExists(paths)).toBe(false); + }); + + test("writeConfig creates directory and file", async () => { + await writeConfig(makeTestConfig(), paths); + expect(await configExists(paths)).toBe(true); + }); + + test("readConfig roundtrips correctly", async () => { + const original = makeTestConfig({ + domain: "https://roundtrip.example.com", + email: "test@example.com", + compose_sha: "abc123", + compose_envs: { LOG_LEVEL: "debug" }, + environment: "dev", + release_channel: "dev", + testnet: true, + link_secret: "mysecret", + analytics: true, + gateway: true, + last_script_run: "2026-01-15T10:30:00Z", + }); + await writeConfig(original, paths); + expect(await readConfig(paths)).toEqual(original); + }); + + test("writeConfig overwrites existing config", async () => { + await writeConfig(makeTestConfig({ domain: "https://first.com" }), paths); + await writeConfig(makeTestConfig({ domain: "https://second.com" }), paths); + expect((await readConfig(paths)).domain).toBe("https://second.com"); + }); + + test("config file is pretty-printed JSON ending with newline", async () => { + await writeConfig(makeTestConfig(), paths); + const raw = await readFile(paths.configPath, "utf-8"); + expect(raw).toContain("\n"); + expect(raw).toContain(" "); + expect(raw.endsWith("\n")).toBe(true); + expect(() => JSON.parse(raw)).not.toThrow(); + }); + + test("readConfig throws on missing file", async () => { + await expect(readConfig(paths)).rejects.toThrow(); + }); + + test("readConfig throws on invalid JSON", async () => { + await mkdir(paths.seedDir, { recursive: true }); + await writeFile(paths.configPath, "not json", "utf-8"); + await expect(readConfig(paths)).rejects.toThrow(); + }); + + test("config preserves all SeedConfig fields", async () => { + await writeConfig(makeTestConfig(), paths); + const loaded = await readConfig(paths); + const expectedKeys: (keyof SeedConfig)[] = [ + "domain", + "email", + "compose_url", + "compose_sha", + "compose_envs", + "environment", + "release_channel", + "testnet", + "link_secret", + "analytics", + "gateway", + "last_script_run", + ]; + for (const key of expectedKeys) { + expect(loaded).toHaveProperty(key); + } + }); +}); + +// --------------------------------------------------------------------------- +// ensureSeedDir +// --------------------------------------------------------------------------- + +describe("ensureSeedDir", () => { + let tmpDir: string; + + beforeEach(async () => { + tmpDir = await mkdtemp(join(tmpdir(), "seed-dir-test-")); + }); + + afterEach(async () => { + await rm(tmpDir, { recursive: true, force: true }); + }); + + test("creates directory if it doesn't exist", async () => { + const seedDir = join(tmpDir, "newseed"); + const paths = makePaths(seedDir); + const shell = makeNoopShell(); + + await ensureSeedDir(paths, shell); + await access(seedDir); // should not throw + }); + + test("succeeds if directory already exists", async () => { + const paths = makePaths(tmpDir); + const shell = makeNoopShell(); + + await ensureSeedDir(paths, shell); + await access(tmpDir); // should not throw + }); + + test("creates nested directory structure", async () => { + const seedDir = join(tmpDir, "deep", "nested", "seed"); + const paths = makePaths(seedDir); + const shell = makeNoopShell(); + + await ensureSeedDir(paths, shell); + await access(seedDir); + }); +}); + +// --------------------------------------------------------------------------- +// checkContainersHealthy / getContainerImages (mock shell) +// --------------------------------------------------------------------------- + +describe("checkContainersHealthy", () => { + test("false when no Docker available", async () => { + expect(await checkContainersHealthy(makeNoopShell())).toBe(false); + }); + + test("false when some containers missing", async () => { + const shell = makeMockShell({ + "seed-proxy": "true", + "seed-web": "true", + }); + expect(await checkContainersHealthy(shell)).toBe(false); + }); + + test("true when all containers running", async () => { + const shell = makeMockShell({ + "seed-proxy": "true", + "seed-web": "true", + "seed-daemon": "true", + }); + expect(await checkContainersHealthy(shell)).toBe(true); + }); + + test("false when a container reports not running", async () => { + const shell = makeMockShell({ + "seed-proxy": "true", + "seed-web": "false", + "seed-daemon": "true", + }); + expect(await checkContainersHealthy(shell)).toBe(false); + }); +}); + +describe("getContainerImages", () => { + test("empty map when no Docker available", async () => { + expect((await getContainerImages(makeNoopShell())).size).toBe(0); + }); + + test("returns images for running containers", async () => { + const shell = makeMockShell({ + "seed-proxy": "sha256:abc123", + "seed-web": "sha256:def456", + "seed-daemon": "sha256:ghi789", + }); + const images = await getContainerImages(shell); + expect(images.size).toBe(3); + expect(images.get("seed-proxy")).toBe("sha256:abc123"); + expect(images.get("seed-web")).toBe("sha256:def456"); + expect(images.get("seed-daemon")).toBe("sha256:ghi789"); + }); +}); + +// --------------------------------------------------------------------------- +// makeShellRunner (real shell smoke tests) +// --------------------------------------------------------------------------- + +describe("makeShellRunner", () => { + test("run executes a basic command", () => { + expect(makeShellRunner().run("echo hello")).toBe("hello"); + }); + + test("runSafe returns null on failure", () => { + expect(makeShellRunner().runSafe("false")).toBeNull(); + }); + + test("exec resolves with stdout", async () => { + expect((await makeShellRunner().exec("echo async")).stdout).toBe("async"); + }); + + test("exec rejects on failure", async () => { + await expect(makeShellRunner().exec("false")).rejects.toThrow(); + }); +}); + +// --------------------------------------------------------------------------- +// Full scenario: config -> compose env +// --------------------------------------------------------------------------- + +describe("full config scenarios", () => { + let tmpDir: string; + let paths: DeployPaths; + + beforeEach(async () => { + tmpDir = await mkdtemp(join(tmpdir(), "seed-scenario-")); + paths = makePaths(tmpDir); + }); + + afterEach(async () => { + await rm(tmpDir, { recursive: true, force: true }); + }); + + test("testnet config roundtrips and produces correct env", async () => { + const config = makeTestConfig({ + domain: "https://dev.hyper.media", + testnet: true, + release_channel: "dev", + gateway: true, + analytics: true, + compose_envs: { LOG_LEVEL: "debug" }, + }); + + await writeConfig(config, paths); + const loaded = await readConfig(paths); + const env = buildComposeEnv(loaded, paths); + + expect(env).toContain('SEED_SITE_HOSTNAME="https://dev.hyper.media"'); + expect(env).toContain('SEED_SITE_DNS="dev.hyper.media"'); + expect(env).toContain('SEED_SITE_TAG="dev"'); + expect(env).toContain('SEED_P2P_TESTNET_NAME="dev"'); + expect(env).toContain(`SEED_LIGHTNING_URL="${LIGHTNING_URL_TESTNET}"`); + expect(env).toContain('SEED_IS_GATEWAY="true"'); + expect(env).toContain('SEED_ENABLE_STATISTICS="true"'); + expect(env).toContain('SEED_LOG_LEVEL="debug"'); + }); + + test("production config roundtrips and produces correct env", async () => { + const config = makeTestConfig({ + domain: "https://node.example.com", + testnet: false, + release_channel: "latest", + gateway: false, + }); + + await writeConfig(config, paths); + const loaded = await readConfig(paths); + const env = buildComposeEnv(loaded, paths); + + expect(env).toContain('SEED_SITE_TAG="latest"'); + expect(env).toContain('SEED_P2P_TESTNET_NAME=""'); + expect(env).toContain(`SEED_LIGHTNING_URL="${LIGHTNING_URL_MAINNET}"`); + expect(env).toContain('SEED_IS_GATEWAY="false"'); + }); + + test("workspace dirs always include monitoring subdirs for daemon volumes", () => { + const dirs = getWorkspaceDirs(paths); + expect(dirs.some((d) => d.includes("monitoring"))).toBe(true); + expect(dirs.some((d) => d.includes("monitoring/grafana"))).toBe(true); + expect(dirs.some((d) => d.includes("monitoring/prometheus"))).toBe(true); + }); +}); diff --git a/ops/deploy.ts b/ops/deploy.ts new file mode 100644 index 000000000..d46f0201d --- /dev/null +++ b/ops/deploy.ts @@ -0,0 +1,1046 @@ +#!/usr/bin/env bun +/** + * Seed Node Deployment Script + * + * Manages the full lifecycle of a self-hosted Seed node: + * - Fresh install wizard (interactive prompts for first-time setup) + * - Migration wizard (detects old installations and migrates config) + * - Headless deployment engine (idempotent, safe to run via cron) + * + * The presence of config.json in the seed directory is the single marker + * of the new deployment system. When it exists, the script runs headless. + * When it doesn't, it runs the interactive wizard. + * + * The seed directory defaults to /opt/seed but can be overridden via + * the SEED_DIR environment variable. + */ + +import * as p from "@clack/prompts"; +import { readFile, writeFile, mkdir, access } from "node:fs/promises"; +import { execSync, exec as execCb } from "node:child_process"; +import { createHash, randomBytes } from "node:crypto"; +import { homedir } from "node:os"; +import { join } from "node:path"; + +export const VERSION = "0.1.0"; +export const DEFAULT_SEED_DIR = process.env.SEED_DIR || "/opt/seed"; +export const DEFAULT_REPO_URL = + "https://raw.githubusercontent.com/seed-hypermedia/seed/main"; +export const DEFAULT_COMPOSE_URL = `${ + process.env.SEED_REPO_URL || DEFAULT_REPO_URL +}/ops/docker-compose.yml`; +export const NOTIFY_SERVICE_HOST = "https://notify.seed.hyper.media"; +export const LIGHTNING_URL_MAINNET = "https://ln.seed.hyper.media"; +export const LIGHTNING_URL_TESTNET = "https://ln.testnet.seed.hyper.media"; + +// --------------------------------------------------------------------------- +// Configurable paths β€” allows tests to inject a temp directory +// --------------------------------------------------------------------------- + +export interface DeployPaths { + seedDir: string; + configPath: string; + composePath: string; + deployLog: string; +} + +export function makePaths(seedDir: string = DEFAULT_SEED_DIR): DeployPaths { + return { + seedDir, + configPath: join(seedDir, "config.json"), + composePath: join(seedDir, "docker-compose.yml"), + deployLog: join(seedDir, "deploy.log"), + }; +} + +// --------------------------------------------------------------------------- +// Shell command abstraction β€” allows tests to inject mocks +// --------------------------------------------------------------------------- + +export interface ShellRunner { + run(cmd: string): string; + runSafe(cmd: string): string | null; + exec(cmd: string): Promise<{ stdout: string; stderr: string }>; +} + +export function makeShellRunner(): ShellRunner { + return { + run(cmd: string): string { + return execSync(cmd, { encoding: "utf-8", timeout: 30_000 }).trim(); + }, + runSafe(cmd: string): string | null { + try { + return this.run(cmd); + } catch { + return null; + } + }, + exec(cmd: string): Promise<{ stdout: string; stderr: string }> { + return new Promise((resolve, reject) => { + execCb(cmd, { timeout: 120_000 }, (err, stdout, stderr) => { + if (err) reject(err); + else + resolve({ + stdout: stdout.toString().trim(), + stderr: stderr.toString().trim(), + }); + }); + }); + }, + }; +} + +export interface SeedConfig { + /** Public hostname for this node, e.g. "https://node1.seed.run" */ + domain: string; + /** Contact email β€” used to reach the operator about security updates */ + email: string; + /** URL to fetch the docker-compose.yml from */ + compose_url: string; + /** SHA-256 of the last deployed docker-compose.yml β€” used to detect changes */ + compose_sha: string; + /** Environment variables passed through to compose services */ + compose_envs: { + LOG_LEVEL: "debug" | "info" | "warn" | "error"; + }; + /** Deployment environment label */ + environment: "dev" | "staging" | "prod"; + /** Docker image tag to pull: "latest" for stable, "dev" for main branch */ + release_channel: "latest" | "dev" | string; + /** Whether to connect to the testnet P2P network instead of mainnet */ + testnet: boolean; + /** Random secret used for the initial site registration URL */ + link_secret: string; + /** Whether to enable Plausible.io web analytics for this site */ + analytics: boolean; + /** Whether this node acts as a public gateway serving all known content */ + gateway: boolean; + /** ISO 8601 timestamp of the last successful deployment */ + last_script_run: string; +} + +/** + * Derives testnet and release_channel from the environment label. + * Keeps the wizard to a single "Environment" question instead of three. + */ +export function environmentPresets(env: SeedConfig["environment"]): { + testnet: boolean; + release_channel: string; +} { + switch (env) { + case "dev": + return { testnet: true, release_channel: "dev" }; + case "staging": + return { testnet: false, release_channel: "dev" }; + case "prod": + default: + return { testnet: false, release_channel: "latest" }; + } +} + +export async function configExists(paths: DeployPaths): Promise { + try { + await access(paths.configPath); + return true; + } catch { + return false; + } +} + +export async function readConfig(paths: DeployPaths): Promise { + const raw = await readFile(paths.configPath, "utf-8"); + return JSON.parse(raw) as SeedConfig; +} + +export async function writeConfig( + config: SeedConfig, + paths: DeployPaths, +): Promise { + await mkdir(paths.seedDir, { recursive: true }); + await writeFile( + paths.configPath, + JSON.stringify(config, null, 2) + "\n", + "utf-8", + ); +} + +export function generateSecret(length = 10): string { + const chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + const bytes = randomBytes(length); + return Array.from(bytes) + .map((b) => chars[b % chars.length]) + .join(""); +} + +export function log(msg: string): void { + const ts = new Date().toISOString(); + if (!process.stdout.isTTY) { + console.log(`[${ts}] ${msg}`); + } +} + +// --------------------------------------------------------------------------- +// Old installation detection +// --------------------------------------------------------------------------- + +export interface OldInstallInfo { + workspace: string; + secret: string | null; + hostname: string | null; + logLevel: string | null; + imageTag: string | null; + testnet: boolean; + gateway: boolean; + trafficStats: boolean; +} + +export function parseDaemonEnv(envJson: string): { + logLevel: string | null; + testnet: boolean; +} { + let logLevel: string | null = null; + let testnet = false; + try { + const envs: string[] = JSON.parse(envJson); + for (const e of envs) { + if (e.startsWith("SEED_LOG_LEVEL=")) logLevel = e.split("=")[1]; + if (e.startsWith("SEED_P2P_TESTNET_NAME=") && e.split("=")[1]) + testnet = true; + } + } catch { + // invalid JSON from docker inspect β€” skip + } + return { logLevel, testnet }; +} + +export function parseWebEnv(envJson: string): { + hostname: string | null; + gateway: boolean; + trafficStats: boolean; +} { + let hostname: string | null = null; + let gateway = false; + let trafficStats = false; + try { + const envs: string[] = JSON.parse(envJson); + for (const e of envs) { + if (e.startsWith("SEED_BASE_URL=")) hostname = e.split("=")[1]; + if (e.startsWith("SEED_IS_GATEWAY=true")) gateway = true; + if (e.startsWith("SEED_ENABLE_STATISTICS=true")) trafficStats = true; + } + } catch { + // invalid JSON from docker inspect β€” skip + } + return { hostname, gateway, trafficStats }; +} + +/** Extract image tag from a Docker image string, e.g. "seedhypermedia/web:latest" -> "latest" */ +export function parseImageTag(imageStr: string): string { + const parts = imageStr.split(":"); + return parts.length > 1 ? parts[parts.length - 1] : "latest"; +} + +export async function detectOldInstall( + shell: ShellRunner, +): Promise { + const home = homedir(); + const candidates = [join(home, ".seed-site"), "/shm/gateway", "/shm"]; + + let workspace: string | null = null; + for (const dir of candidates) { + try { + await access(dir); + workspace = dir; + break; + } catch { + // not found, try next + } + } + + const hasContainers = shell.runSafe( + "docker ps --format '{{.Names}}' 2>/dev/null | grep -q seed", + ); + if (!workspace && hasContainers === null) { + return null; + } + + if (!workspace) { + workspace = join(home, ".seed-site"); + } + + let secret: string | null = null; + const secretPaths = [ + join(workspace, "web", "config.json"), + "/shm/gateway/web/config.json", + join(home, ".seed-site", "web", "config.json"), + ]; + for (const sp of secretPaths) { + try { + const raw = await readFile(sp, "utf-8"); + const parsed = JSON.parse(raw); + if (parsed.availableRegistrationSecret) { + secret = parsed.availableRegistrationSecret; + break; + } + } catch { + // try next + } + } + + let hostname: string | null = null; + let logLevel: string | null = null; + let imageTag: string | null = null; + let testnet = false; + let gateway = false; + let trafficStats = false; + + const daemonEnv = shell.runSafe( + "docker inspect seed-daemon --format '{{json .Config.Env}}' 2>/dev/null", + ); + if (daemonEnv) { + const parsed = parseDaemonEnv(daemonEnv); + logLevel = parsed.logLevel; + testnet = parsed.testnet; + } + + const webEnv = shell.runSafe( + "docker inspect seed-web --format '{{json .Config.Env}}' 2>/dev/null", + ); + if (webEnv) { + const parsed = parseWebEnv(webEnv); + hostname = parsed.hostname; + gateway = parsed.gateway; + trafficStats = parsed.trafficStats; + } + + const webImage = shell.runSafe( + "docker inspect seed-web --format '{{.Config.Image}}' 2>/dev/null", + ); + if (webImage) { + imageTag = parseImageTag(webImage); + } + + return { + workspace, + secret, + hostname, + logLevel, + imageTag, + testnet, + gateway, + trafficStats, + }; +} + +// --------------------------------------------------------------------------- +// Migration Wizard +// --------------------------------------------------------------------------- + +async function runMigrationWizard( + old: OldInstallInfo, + paths: DeployPaths, + shell: ShellRunner, +): Promise { + p.intro(`Seed Node Migration v${VERSION}`); + + p.note( + [ + `Detected an existing Seed installation at: ${old.workspace}`, + "", + "We'll import your current settings and migrate to the new deployment system.", + `After migration, your node will be managed from ${paths.seedDir}/ and updated via cron.`, + "", + "Please review and confirm the detected values below.", + ].join("\n"), + "Existing installation found", + ); + + const answers = await p.group( + { + domain: () => + p.text({ + message: "Public hostname (including https://)", + placeholder: "https://node1.seed.run", + initialValue: old.hostname ?? "", + validate: (v) => { + if (!v) return "Required"; + if (!v.startsWith("https://") && !v.startsWith("http://")) + return "Must start with https:// or http://"; + }, + }), + email: () => + p.text({ + message: + "Contact email β€” lets us notify you about security updates and node issues. Not shared publicly.", + placeholder: "you@example.com", + validate: (v) => { + if (!v) return "Required"; + if (!v.includes("@")) return "Must be a valid email"; + }, + }), + environment: () => + p.select({ + message: "Environment", + initialValue: old.testnet ? "dev" : "prod", + options: [ + { + value: "prod", + label: "Production", + hint: "stable releases, mainnet network β€” recommended", + }, + { + value: "staging", + label: "Staging", + hint: "development builds, mainnet network β€” for testing", + }, + { + value: "dev", + label: "Development", + hint: "development builds, testnet network", + }, + ], + }), + log_level: () => + p.select({ + message: "Log level", + initialValue: old.logLevel ?? "info", + options: [ + { + value: "debug", + label: "Debug", + hint: "verbose, useful for troubleshooting", + }, + { + value: "info", + label: "Info", + hint: "standard operational logging", + }, + { value: "warn", label: "Warn", hint: "only warnings and errors" }, + { value: "error", label: "Error", hint: "only errors" }, + ], + }), + gateway: () => + p.confirm({ + message: "Run as public gateway?", + initialValue: old.gateway, + }), + analytics: () => + p.confirm({ + message: + "Enable web analytics? Adds a Plausible.io dashboard to track your site's traffic.", + initialValue: old.trafficStats, + }), + }, + { + onCancel: () => { + p.cancel("Migration cancelled"); + process.exit(0); + }, + }, + ); + + const secret = old.secret ?? generateSecret(); + if (old.secret) { + p.log.success(`Registration secret imported from existing installation.`); + } else { + p.log.warn(`No existing registration secret found. Generated a new one.`); + } + + const env = answers.environment as SeedConfig["environment"]; + const presets = environmentPresets(env); + + const config: SeedConfig = { + domain: answers.domain as string, + email: answers.email as string, + compose_url: DEFAULT_COMPOSE_URL, + compose_sha: "", + compose_envs: { + LOG_LEVEL: answers.log_level as SeedConfig["compose_envs"]["LOG_LEVEL"], + }, + environment: env, + release_channel: presets.release_channel, + testnet: presets.testnet, + link_secret: secret, + analytics: answers.analytics as boolean, + gateway: answers.gateway as boolean, + last_script_run: "", + }; + + const summary = Object.entries(config) + .map(([k, v]) => ` ${k}: ${typeof v === "object" ? JSON.stringify(v) : v}`) + .join("\n"); + p.note(summary, "Configuration summary"); + + const confirmed = await p.confirm({ + message: "Write config and proceed with deployment?", + }); + + if (p.isCancel(confirmed) || !confirmed) { + p.cancel("Migration cancelled"); + process.exit(0); + } + + await writeConfig(config, paths); + p.log.success(`Config written to ${paths.configPath}`); + + // Migrate file ownership from the old UID 1001 (hardcoded in the previous + // web Dockerfile) to the current user. The new docker-compose.yml runs the + // web container as the host user, so the data directory must be owned by them. + const webDir = join(old.workspace, "web"); + const currentUid = String(process.getuid!()); + const currentGid = String(process.getgid!()); + const owner = shell.runSafe(`stat -c '%u:%g' "${webDir}" 2>/dev/null`); + if (owner && owner !== `${currentUid}:${currentGid}`) { + p.log.warn( + `The web data directory (${webDir}) is owned by a different user (${owner}).` + + ` Updating ownership so the web container can write to it.`, + ); + if ( + !shell.runSafe( + `chown -R ${currentUid}:${currentGid} "${webDir}" 2>/dev/null`, + ) + ) { + shell.runSafe(`sudo chown -R ${currentUid}:${currentGid} "${webDir}"`); + } + p.log.success("File ownership updated."); + } + + return config; +} + +// --------------------------------------------------------------------------- +// Fresh Install Wizard +// --------------------------------------------------------------------------- + +async function runFreshWizard(paths: DeployPaths): Promise { + p.intro(`Seed Node Setup v${VERSION}`); + + p.note( + [ + "Welcome! This wizard will configure your new Seed node.", + "", + "Seed is a peer-to-peer hypermedia publishing system. This script sets up", + "the Docker containers, reverse proxy, and networking so your node is", + "reachable on the public internet.", + "", + `Configuration will be saved to ${paths.configPath}.`, + "Subsequent runs of this script will deploy automatically (headless mode).", + ].join("\n"), + "First-time setup", + ); + + const answers = await p.group( + { + domain: () => + p.text({ + message: "Public hostname (including https://)", + placeholder: "https://node1.seed.run", + validate: (v) => { + if (!v) return "Required"; + if (!v.startsWith("https://") && !v.startsWith("http://")) + return "Must start with https:// or http://"; + }, + }), + email: () => + p.text({ + message: + "Contact email β€” lets us notify you about security updates and node issues. Not shared publicly.", + placeholder: "you@example.com", + validate: (v) => { + if (!v) return "Required"; + if (!v.includes("@")) return "Must be a valid email"; + }, + }), + environment: () => + p.select({ + message: "Environment", + initialValue: "prod", + options: [ + { + value: "prod", + label: "Production", + hint: "stable releases, mainnet network β€” recommended", + }, + { + value: "staging", + label: "Staging", + hint: "development builds, mainnet network β€” for testing", + }, + { + value: "dev", + label: "Development", + hint: "development builds, testnet network", + }, + ], + }), + log_level: () => + p.select({ + message: "Log level for Seed services", + initialValue: "info", + options: [ + { + value: "debug", + label: "Debug", + hint: "very verbose, useful for troubleshooting", + }, + { + value: "info", + label: "Info", + hint: "standard operational logging β€” recommended", + }, + { value: "warn", label: "Warn", hint: "only warnings and errors" }, + { value: "error", label: "Error", hint: "only critical errors" }, + ], + }), + gateway: () => + p.confirm({ + message: "Run as a public gateway? (serves all known public content)", + initialValue: false, + }), + analytics: () => + p.confirm({ + message: + "Enable web analytics? Adds a Plausible.io dashboard to track your site's traffic.", + initialValue: false, + }), + }, + { + onCancel: () => { + p.cancel("Setup cancelled"); + process.exit(0); + }, + }, + ); + + const secret = generateSecret(); + + const env = answers.environment as SeedConfig["environment"]; + const presets = environmentPresets(env); + + const config: SeedConfig = { + domain: answers.domain as string, + email: answers.email as string, + compose_url: DEFAULT_COMPOSE_URL, + compose_sha: "", + compose_envs: { + LOG_LEVEL: answers.log_level as SeedConfig["compose_envs"]["LOG_LEVEL"], + }, + environment: env, + release_channel: presets.release_channel, + testnet: presets.testnet, + link_secret: secret, + analytics: answers.analytics as boolean, + gateway: answers.gateway as boolean, + last_script_run: "", + }; + + const summary = Object.entries(config) + .filter(([k]) => k !== "compose_sha" && k !== "last_script_run") + .map(([k, v]) => ` ${k}: ${typeof v === "object" ? JSON.stringify(v) : v}`) + .join("\n"); + p.note(summary, "Configuration summary"); + + const confirmed = await p.confirm({ + message: "Write config and proceed with deployment?", + }); + + if (p.isCancel(confirmed) || !confirmed) { + p.cancel("Setup cancelled"); + process.exit(0); + } + + await writeConfig(config, paths); + p.log.success(`Config written to ${paths.configPath}`); + + return config; +} + +// --------------------------------------------------------------------------- +// Deployment Engine +// --------------------------------------------------------------------------- + +/** Extract the bare DNS name from a URL, e.g. "https://node.seed.run" -> "node.seed.run" */ +export function extractDns(domain: string): string { + return domain.replace(/^https?:\/\//, "").replace(/\/+$/, ""); +} + +export function generateCaddyfile(_config: SeedConfig): string { + return `{$SEED_SITE_HOSTNAME} + +encode zstd gzip + +@ipfsget { +\tmethod GET HEAD OPTIONS +\tpath /ipfs/* +} + +reverse_proxy /.metrics* grafana:{$SEED_SITE_MONITORING_PORT:3001} + +reverse_proxy @ipfsget seed-daemon:{$HM_SITE_BACKEND_GRPCWEB_PORT:56001} + +reverse_proxy * seed-web:{$SEED_SITE_LOCAL_PORT:3000} +`; +} + +export function sha256(content: string): string { + return createHash("sha256").update(content).digest("hex"); +} + +export async function checkContainersHealthy( + shell: ShellRunner, +): Promise { + const required = ["seed-proxy", "seed-web", "seed-daemon"]; + for (const name of required) { + const running = shell.runSafe( + `docker inspect ${name} --format '{{.State.Running}}' 2>/dev/null`, + ); + if (running !== "true") return false; + } + return true; +} + +export async function getContainerImages( + shell: ShellRunner, +): Promise> { + const images = new Map(); + const containers = ["seed-proxy", "seed-web", "seed-daemon"]; + for (const name of containers) { + const image = shell.runSafe( + `docker inspect ${name} --format '{{.Image}}' 2>/dev/null`, + ); + if (image) images.set(name, image); + } + return images; +} + +export function buildComposeEnv( + config: SeedConfig, + paths: DeployPaths, +): string { + const dns = extractDns(config.domain); + const testnetName = config.testnet ? "dev" : ""; + const lightningUrl = config.testnet + ? LIGHTNING_URL_TESTNET + : LIGHTNING_URL_MAINNET; + + const vars: Record = { + SEED_SITE_HOSTNAME: config.domain, + SEED_SITE_DNS: dns, + SEED_SITE_TAG: config.release_channel, + SEED_SITE_WORKSPACE: paths.seedDir, + SEED_UID: String(process.getuid!()), + SEED_GID: String(process.getgid!()), + SEED_LOG_LEVEL: config.compose_envs.LOG_LEVEL, + SEED_IS_GATEWAY: String(config.gateway), + SEED_ENABLE_STATISTICS: String(config.analytics), + SEED_P2P_TESTNET_NAME: testnetName, + SEED_LIGHTNING_URL: lightningUrl, + NOTIFY_SERVICE_HOST: NOTIFY_SERVICE_HOST, + SEED_SITE_MONITORING_WORKDIR: join(paths.seedDir, "monitoring"), + }; + + return Object.entries(vars) + .map(([k, v]) => `${k}="${v}"`) + .join(" "); +} + +export function getWorkspaceDirs(paths: DeployPaths): string[] { + // The daemon container always mounts monitoring volumes (for rsync of + // built-in dashboards), so these directories must exist even when the + // monitoring profile isn't active. + return [ + join(paths.seedDir, "proxy"), + join(paths.seedDir, "proxy", "data"), + join(paths.seedDir, "proxy", "config"), + join(paths.seedDir, "web"), + join(paths.seedDir, "daemon"), + join(paths.seedDir, "monitoring"), + join(paths.seedDir, "monitoring", "grafana"), + join(paths.seedDir, "monitoring", "prometheus"), + ]; +} + +/** + * Ensures the seed directory exists and is writable by the current user. + * Uses sudo only when the parent directory isn't writable. + */ +export async function ensureSeedDir( + paths: DeployPaths, + shell: ShellRunner, +): Promise { + try { + await access(paths.seedDir); + } catch { + // Directory doesn't exist β€” try creating it, escalate to sudo if needed + try { + await mkdir(paths.seedDir, { recursive: true }); + } catch { + log(`Creating ${paths.seedDir} requires elevated permissions`); + shell.run(`sudo mkdir -p "${paths.seedDir}"`); + shell.run(`sudo chown "$(id -u):$(id -g)" "${paths.seedDir}"`); + } + } +} + +async function rollback( + previousImages: Map, + config: SeedConfig, + paths: DeployPaths, + shell: ShellRunner, +): Promise { + log("Deployment failed β€” rolling back to previous images..."); + for (const [name, imageId] of previousImages) { + log(` Restoring ${name} to image ${imageId.slice(0, 16)}...`); + shell.runSafe(`docker stop ${name} 2>/dev/null`); + shell.runSafe(`docker rm ${name} 2>/dev/null`); + } + log(" Running docker compose up with cached images..."); + const env = buildComposeEnv(config, paths); + shell.runSafe( + `${env} docker compose -f ${paths.composePath} up -d --quiet-pull 2>&1`, + ); + log("Rollback complete. Check container status with: docker ps"); +} + +export async function deploy( + config: SeedConfig, + paths: DeployPaths, + shell: ShellRunner, +): Promise { + const isInteractive = process.stdout.isTTY; + const spinner = isInteractive ? p.spinner() : null; + + const step = (msg: string) => { + if (spinner) spinner.message(msg); + log(msg); + }; + + if (isInteractive) { + p.log.step("Starting deployment..."); + } + + spinner?.start("Fetching docker-compose.yml..."); + step("Fetching docker-compose.yml..."); + + const repoOverride = process.env.SEED_REPO_URL; + const composeUrl = repoOverride + ? `${repoOverride}/ops/docker-compose.yml` + : config.compose_url; + const composeResponse = await fetch(composeUrl); + if (!composeResponse.ok) { + spinner?.stop("Failed to fetch docker-compose.yml"); + throw new Error( + `Failed to fetch compose file from ${composeUrl}: ${composeResponse.status}`, + ); + } + const composeContent = await composeResponse.text(); + const composeSha = sha256(composeContent); + + const containersHealthy = await checkContainersHealthy(shell); + if (config.compose_sha === composeSha && containersHealthy) { + spinner?.stop( + "No changes detected β€” all containers healthy. Skipping redeployment.", + ); + log( + "No changes detected β€” compose SHA matches and containers are healthy. Skipping.", + ); + config.last_script_run = new Date().toISOString(); + await writeConfig(config, paths); + return; + } + + if (config.compose_sha && config.compose_sha !== composeSha) { + step( + `Compose file changed: ${config.compose_sha.slice(0, 8)} -> ${composeSha.slice(0, 8)}`, + ); + } + + await ensureSeedDir(paths, shell); + await writeFile(paths.composePath, composeContent, "utf-8"); + + step("Setting up workspace directories..."); + const dirs = getWorkspaceDirs(paths); + for (const dir of dirs) { + await mkdir(dir, { recursive: true }); + } + + step("Generating Caddyfile..."); + const caddyfile = generateCaddyfile(config); + await writeFile( + join(paths.seedDir, "proxy", "CaddyFile"), + caddyfile, + "utf-8", + ); + + // Write registration secret only on first deploy (web/config.json doesn't exist yet) + const webConfigPath = join(paths.seedDir, "web", "config.json"); + let isFirstDeploy = false; + try { + await access(webConfigPath); + } catch { + isFirstDeploy = true; + await writeFile( + webConfigPath, + JSON.stringify({ availableRegistrationSecret: config.link_secret }) + + "\n", + "utf-8", + ); + step("Created initial web/config.json with registration secret."); + } + + step("Stopping any existing containers..."); + shell.runSafe( + "docker stop seed-site seed-daemon seed-proxy grafana prometheus 2>/dev/null", + ); + shell.runSafe( + "docker rm seed-site seed-daemon seed-proxy grafana prometheus 2>/dev/null", + ); + + const previousImages = await getContainerImages(shell); + + step("Running docker compose up..."); + const env = buildComposeEnv(config, paths); + + try { + const composeCmd = `${env} docker compose -f ${paths.composePath} up -d --pull always --quiet-pull`; + const result = await shell.exec(composeCmd); + if (result.stderr) { + log(`compose stderr: ${result.stderr}`); + } + } catch (err: unknown) { + spinner?.stop("docker compose up failed"); + log(`docker compose up failed: ${err}`); + if (previousImages.size > 0) { + await rollback(previousImages, config, paths, shell); + } + throw new Error(`Deployment failed: ${err}`); + } + + step("Running post-deploy health checks..."); + let healthy = false; + for (let attempt = 0; attempt < 10; attempt++) { + await new Promise((r) => setTimeout(r, 3000)); + healthy = await checkContainersHealthy(shell); + if (healthy) break; + step(`Health check attempt ${attempt + 1}/10...`); + } + + if (!healthy) { + spinner?.stop("Health checks failed"); + log("Health checks failed β€” containers not running after 30s"); + if (previousImages.size > 0) { + await rollback(previousImages, config, paths, shell); + } + throw new Error( + "Deployment failed: containers did not become healthy within 30 seconds", + ); + } + + config.compose_sha = composeSha; + config.last_script_run = new Date().toISOString(); + await writeConfig(config, paths); + + spinner?.stop("Deployment complete!"); + log("Deployment complete."); + + if (isInteractive && isFirstDeploy) { + // TODO: POST config.email, config.domain, and config.analytics to the + // Seed vault so the team knows about new deployments and can activate + // Plausible analytics for this domain. Coordinate endpoint with Eric. + + p.note( + [ + `Your site is live at ${config.domain}`, + "", + ` Secret: ${config.link_secret}`, + "", + "Open the Seed desktop app and enter this secret to link", + "your publisher account to this site.", + ].join("\n"), + "Setup complete", + ); + } +} + +// --------------------------------------------------------------------------- +// Cron Setup +// --------------------------------------------------------------------------- + +export async function setupCron( + paths: DeployPaths, + shell: ShellRunner, +): Promise { + const cronLine = `0 2 * * * /usr/bin/bun ${join(paths.seedDir, "deploy.js")} >> ${paths.deployLog} 2>&1 # seed-deploy`; + + const existing = shell.runSafe("crontab -l 2>/dev/null") ?? ""; + if (existing.includes("seed-deploy")) { + log("Cron job already installed. Skipping."); + return; + } + + const cleanupCron = `0 0,4,8,12,16,20 * * * docker image prune -a -f # seed-cleanup`; + const newCrontab = + [existing, cronLine, cleanupCron].filter(Boolean).join("\n") + "\n"; + try { + execSync(`echo '${newCrontab}' | crontab -`, { encoding: "utf-8" }); + log( + "Installed nightly deployment cron job (02:00) and image cleanup cron.", + ); + } catch (err) { + log(`Warning: Failed to install cron job: ${err}`); + } +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +async function main(): Promise { + const paths = makePaths(); + const shell = makeShellRunner(); + + await ensureSeedDir(paths, shell); + + const hasConfig = await configExists(paths); + + if (hasConfig) { + log( + `Seed deploy v${VERSION} β€” config found at ${paths.configPath}, running headless.`, + ); + const config = await readConfig(paths); + await deploy(config, paths, shell); + return; + } + + const oldInstall = await detectOldInstall(shell); + + let config: SeedConfig; + if (oldInstall) { + config = await runMigrationWizard(oldInstall, paths, shell); + } else { + config = await runFreshWizard(paths); + } + + const wantsCron = await p.confirm({ + message: "Install nightly cron job for automatic updates? (runs at 02:00)", + initialValue: true, + }); + if (!p.isCancel(wantsCron) && wantsCron) { + await setupCron(paths, shell); + p.log.success( + "Cron job installed. Your node will auto-update nightly at 02:00.", + ); + } + + await deploy(config, paths, shell); + + p.outro("Setup complete! Your Seed node is running."); +} + +if (import.meta.main) { + main().catch((err) => { + console.error("Fatal error:", err); + process.exit(1); + }); +} diff --git a/ops/dist/deploy.js b/ops/dist/deploy.js new file mode 100755 index 000000000..70ec20cf4 --- /dev/null +++ b/ops/dist/deploy.js @@ -0,0 +1,79 @@ +#!/usr/bin/env bun +// @bun +var Kb=Object.create;var{getPrototypeOf:Sb,defineProperty:M1,getOwnPropertyNames:Yb}=Object;var Qb=Object.prototype.hasOwnProperty;var h=(x,$,B)=>{B=x!=null?Kb(Sb(x)):{};let K=$||!x||!x.__esModule?M1(B,"default",{value:x,enumerable:!0}):B;for(let S of Yb(x))if(!Qb.call(K,S))M1(K,S,{get:()=>x[S],enumerable:!0});return K};var N1=(x,$)=>()=>($||x(($={exports:{}}).exports,$),$.exports);var e=N1((Nx,P1)=>{var s={to(x,$){if(!$)return`\x1B[${x+1}G`;return`\x1B[${$+1};${x+1}H`},move(x,$){let B="";if(x<0)B+=`\x1B[${-x}D`;else if(x>0)B+=`\x1B[${x}C`;if($<0)B+=`\x1B[${-$}A`;else if($>0)B+=`\x1B[${$}B`;return B},up:(x=1)=>`\x1B[${x}A`,down:(x=1)=>`\x1B[${x}B`,forward:(x=1)=>`\x1B[${x}C`,backward:(x=1)=>`\x1B[${x}D`,nextLine:(x=1)=>"\x1B[E".repeat(x),prevLine:(x=1)=>"\x1B[F".repeat(x),left:"\x1B[G",hide:"\x1B[?25l",show:"\x1B[?25h",save:"\x1B7",restore:"\x1B8"},Xb={up:(x=1)=>"\x1B[S".repeat(x),down:(x=1)=>"\x1B[T".repeat(x)},Jb={screen:"\x1B[2J",up:(x=1)=>"\x1B[1J".repeat(x),down:(x=1)=>"\x1B[J".repeat(x),line:"\x1B[2K",lineEnd:"\x1B[K",lineStart:"\x1B[1K",lines(x){let $="";for(let B=0;B{var F=process||{},R1=F.argv||[],l=F.env||{},Zb=!(!!l.NO_COLOR||R1.includes("--no-color"))&&(!!l.FORCE_COLOR||R1.includes("--color")||F.platform==="win32"||(F.stdout||{}).isTTY&&l.TERM!=="dumb"||!!l.CI),qb=(x,$,B=x)=>(K)=>{let S=""+K,Y=S.indexOf($,x.length);return~Y?x+zb(S,$,B,Y)+$:x+S+$},zb=(x,$,B,K)=>{let S="",Y=0;do S+=x.substring(Y,K)+B,Y=K+$.length,K=x.indexOf($,Y);while(~K);return S+x.substring(Y)},T1=(x=Zb)=>{let $=x?qb:()=>String;return{isColorSupported:x,reset:$("\x1B[0m","\x1B[0m"),bold:$("\x1B[1m","\x1B[22m","\x1B[22m\x1B[1m"),dim:$("\x1B[2m","\x1B[22m","\x1B[22m\x1B[2m"),italic:$("\x1B[3m","\x1B[23m"),underline:$("\x1B[4m","\x1B[24m"),inverse:$("\x1B[7m","\x1B[27m"),hidden:$("\x1B[8m","\x1B[28m"),strikethrough:$("\x1B[9m","\x1B[29m"),black:$("\x1B[30m","\x1B[39m"),red:$("\x1B[31m","\x1B[39m"),green:$("\x1B[32m","\x1B[39m"),yellow:$("\x1B[33m","\x1B[39m"),blue:$("\x1B[34m","\x1B[39m"),magenta:$("\x1B[35m","\x1B[39m"),cyan:$("\x1B[36m","\x1B[39m"),white:$("\x1B[37m","\x1B[39m"),gray:$("\x1B[90m","\x1B[39m"),bgBlack:$("\x1B[40m","\x1B[49m"),bgRed:$("\x1B[41m","\x1B[49m"),bgGreen:$("\x1B[42m","\x1B[49m"),bgYellow:$("\x1B[43m","\x1B[49m"),bgBlue:$("\x1B[44m","\x1B[49m"),bgMagenta:$("\x1B[45m","\x1B[49m"),bgCyan:$("\x1B[46m","\x1B[49m"),bgWhite:$("\x1B[47m","\x1B[49m"),blackBright:$("\x1B[90m","\x1B[39m"),redBright:$("\x1B[91m","\x1B[39m"),greenBright:$("\x1B[92m","\x1B[39m"),yellowBright:$("\x1B[93m","\x1B[39m"),blueBright:$("\x1B[94m","\x1B[39m"),magentaBright:$("\x1B[95m","\x1B[39m"),cyanBright:$("\x1B[96m","\x1B[39m"),whiteBright:$("\x1B[97m","\x1B[39m"),bgBlackBright:$("\x1B[100m","\x1B[49m"),bgRedBright:$("\x1B[101m","\x1B[49m"),bgGreenBright:$("\x1B[102m","\x1B[49m"),bgYellowBright:$("\x1B[103m","\x1B[49m"),bgBlueBright:$("\x1B[104m","\x1B[49m"),bgMagentaBright:$("\x1B[105m","\x1B[49m"),bgCyanBright:$("\x1B[106m","\x1B[49m"),bgWhiteBright:$("\x1B[107m","\x1B[49m")}};b1.exports=T1();b1.exports.createColors=T1});import{stripVTControlCharacters as q1}from"util";var T=h(e(),1),f1=h(x1(),1);import{stdin as E1,stdout as w1}from"process";import*as j from"readline";import m1 from"readline";import{WriteStream as Wb}from"tty";function Gb({onlyFirst:x=!1}={}){let $=["[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?(?:\\u0007|\\u001B\\u005C|\\u009C))","(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))"].join("|");return new RegExp($,x?void 0:"g")}var Ib=Gb();function v1(x){if(typeof x!="string")throw TypeError(`Expected a \`string\`, got \`${typeof x}\``);return x.replace(Ib,"")}function d1(x){return x&&x.__esModule&&Object.prototype.hasOwnProperty.call(x,"default")?x.default:x}var g1={exports:{}};(function(x){var $={};x.exports=$,$.eastAsianWidth=function(K){var S=K.charCodeAt(0),Y=K.length==2?K.charCodeAt(1):0,b=S;return 55296<=S&&S<=56319&&56320<=Y&&Y<=57343&&(S&=1023,Y&=1023,b=S<<10|Y,b+=65536),b==12288||65281<=b&&b<=65376||65504<=b&&b<=65510?"F":b==8361||65377<=b&&b<=65470||65474<=b&&b<=65479||65482<=b&&b<=65487||65490<=b&&b<=65495||65498<=b&&b<=65500||65512<=b&&b<=65518?"H":4352<=b&&b<=4447||4515<=b&&b<=4519||4602<=b&&b<=4607||9001<=b&&b<=9002||11904<=b&&b<=11929||11931<=b&&b<=12019||12032<=b&&b<=12245||12272<=b&&b<=12283||12289<=b&&b<=12350||12353<=b&&b<=12438||12441<=b&&b<=12543||12549<=b&&b<=12589||12593<=b&&b<=12686||12688<=b&&b<=12730||12736<=b&&b<=12771||12784<=b&&b<=12830||12832<=b&&b<=12871||12880<=b&&b<=13054||13056<=b&&b<=19903||19968<=b&&b<=42124||42128<=b&&b<=42182||43360<=b&&b<=43388||44032<=b&&b<=55203||55216<=b&&b<=55238||55243<=b&&b<=55291||63744<=b&&b<=64255||65040<=b&&b<=65049||65072<=b&&b<=65106||65108<=b&&b<=65126||65128<=b&&b<=65131||110592<=b&&b<=110593||127488<=b&&b<=127490||127504<=b&&b<=127546||127552<=b&&b<=127560||127568<=b&&b<=127569||131072<=b&&b<=194367||177984<=b&&b<=196605||196608<=b&&b<=262141?"W":32<=b&&b<=126||162<=b&&b<=163||165<=b&&b<=166||b==172||b==175||10214<=b&&b<=10221||10629<=b&&b<=10630?"Na":b==161||b==164||167<=b&&b<=168||b==170||173<=b&&b<=174||176<=b&&b<=180||182<=b&&b<=186||188<=b&&b<=191||b==198||b==208||215<=b&&b<=216||222<=b&&b<=225||b==230||232<=b&&b<=234||236<=b&&b<=237||b==240||242<=b&&b<=243||247<=b&&b<=250||b==252||b==254||b==257||b==273||b==275||b==283||294<=b&&b<=295||b==299||305<=b&&b<=307||b==312||319<=b&&b<=322||b==324||328<=b&&b<=331||b==333||338<=b&&b<=339||358<=b&&b<=359||b==363||b==462||b==464||b==466||b==468||b==470||b==472||b==474||b==476||b==593||b==609||b==708||b==711||713<=b&&b<=715||b==717||b==720||728<=b&&b<=731||b==733||b==735||768<=b&&b<=879||913<=b&&b<=929||931<=b&&b<=937||945<=b&&b<=961||963<=b&&b<=969||b==1025||1040<=b&&b<=1103||b==1105||b==8208||8211<=b&&b<=8214||8216<=b&&b<=8217||8220<=b&&b<=8221||8224<=b&&b<=8226||8228<=b&&b<=8231||b==8240||8242<=b&&b<=8243||b==8245||b==8251||b==8254||b==8308||b==8319||8321<=b&&b<=8324||b==8364||b==8451||b==8453||b==8457||b==8467||b==8470||8481<=b&&b<=8482||b==8486||b==8491||8531<=b&&b<=8532||8539<=b&&b<=8542||8544<=b&&b<=8555||8560<=b&&b<=8569||b==8585||8592<=b&&b<=8601||8632<=b&&b<=8633||b==8658||b==8660||b==8679||b==8704||8706<=b&&b<=8707||8711<=b&&b<=8712||b==8715||b==8719||b==8721||b==8725||b==8730||8733<=b&&b<=8736||b==8739||b==8741||8743<=b&&b<=8748||b==8750||8756<=b&&b<=8759||8764<=b&&b<=8765||b==8776||b==8780||b==8786||8800<=b&&b<=8801||8804<=b&&b<=8807||8810<=b&&b<=8811||8814<=b&&b<=8815||8834<=b&&b<=8835||8838<=b&&b<=8839||b==8853||b==8857||b==8869||b==8895||b==8978||9312<=b&&b<=9449||9451<=b&&b<=9547||9552<=b&&b<=9587||9600<=b&&b<=9615||9618<=b&&b<=9621||9632<=b&&b<=9633||9635<=b&&b<=9641||9650<=b&&b<=9651||9654<=b&&b<=9655||9660<=b&&b<=9661||9664<=b&&b<=9665||9670<=b&&b<=9672||b==9675||9678<=b&&b<=9681||9698<=b&&b<=9701||b==9711||9733<=b&&b<=9734||b==9737||9742<=b&&b<=9743||9748<=b&&b<=9749||b==9756||b==9758||b==9792||b==9794||9824<=b&&b<=9825||9827<=b&&b<=9829||9831<=b&&b<=9834||9836<=b&&b<=9837||b==9839||9886<=b&&b<=9887||9918<=b&&b<=9919||9924<=b&&b<=9933||9935<=b&&b<=9953||b==9955||9960<=b&&b<=9983||b==10045||b==10071||10102<=b&&b<=10111||11093<=b&&b<=11097||12872<=b&&b<=12879||57344<=b&&b<=63743||65024<=b&&b<=65039||b==65533||127232<=b&&b<=127242||127248<=b&&b<=127277||127280<=b&&b<=127337||127344<=b&&b<=127386||917760<=b&&b<=917999||983040<=b&&b<=1048573||1048576<=b&&b<=1114109?"A":"N"},$.characterLength=function(K){var S=this.eastAsianWidth(K);return S=="F"||S=="W"||S=="A"?2:1};function B(K){return K.match(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[^\uD800-\uDFFF]/g)||[]}$.length=function(K){for(var S=B(K),Y=0,b=0;b=S-(q==2?1:0))if(X+q<=Y)b+=z;else break;X+=q}return b}})(g1);var Hb=g1.exports,Vb=d1(Hb),Ob=function(){return/\uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62(?:\uDB40\uDC77\uDB40\uDC6C\uDB40\uDC73|\uDB40\uDC73\uDB40\uDC63\uDB40\uDC74|\uDB40\uDC65\uDB40\uDC6E\uDB40\uDC67)\uDB40\uDC7F|(?:\uD83E\uDDD1\uD83C\uDFFF\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFF\u200D\uD83E\uDD1D\u200D(?:\uD83D[\uDC68\uDC69]))(?:\uD83C[\uDFFB-\uDFFE])|(?:\uD83E\uDDD1\uD83C\uDFFE\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFE\u200D\uD83E\uDD1D\u200D(?:\uD83D[\uDC68\uDC69]))(?:\uD83C[\uDFFB-\uDFFD\uDFFF])|(?:\uD83E\uDDD1\uD83C\uDFFD\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFD\u200D\uD83E\uDD1D\u200D(?:\uD83D[\uDC68\uDC69]))(?:\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])|(?:\uD83E\uDDD1\uD83C\uDFFC\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFC\u200D\uD83E\uDD1D\u200D(?:\uD83D[\uDC68\uDC69]))(?:\uD83C[\uDFFB\uDFFD-\uDFFF])|(?:\uD83E\uDDD1\uD83C\uDFFB\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFB\u200D\uD83E\uDD1D\u200D(?:\uD83D[\uDC68\uDC69]))(?:\uD83C[\uDFFC-\uDFFF])|\uD83D\uDC68(?:\uD83C\uDFFB(?:\u200D(?:\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFF])|\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFF]))|\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFC-\uDFFF])|[\u2695\u2696\u2708]\uFE0F|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD]))?|(?:\uD83C[\uDFFC-\uDFFF])\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFF])|\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFF]))|\u200D(?:\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83D\uDC68|(?:\uD83D[\uDC68\uDC69])\u200D(?:\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67]))|\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67])|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFF\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFE])|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFE\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFD\uDFFF])|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFD\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFC\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB\uDFFD-\uDFFF])|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|(?:\uD83C\uDFFF\u200D[\u2695\u2696\u2708]|\uD83C\uDFFE\u200D[\u2695\u2696\u2708]|\uD83C\uDFFD\u200D[\u2695\u2696\u2708]|\uD83C\uDFFC\u200D[\u2695\u2696\u2708]|\u200D[\u2695\u2696\u2708])\uFE0F|\u200D(?:(?:\uD83D[\uDC68\uDC69])\u200D(?:\uD83D[\uDC66\uDC67])|\uD83D[\uDC66\uDC67])|\uD83C\uDFFF|\uD83C\uDFFE|\uD83C\uDFFD|\uD83C\uDFFC)?|(?:\uD83D\uDC69(?:\uD83C\uDFFB\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D(?:\uD83D[\uDC68\uDC69])|\uD83D[\uDC68\uDC69])|(?:\uD83C[\uDFFC-\uDFFF])\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D(?:\uD83D[\uDC68\uDC69])|\uD83D[\uDC68\uDC69]))|\uD83E\uDDD1(?:\uD83C[\uDFFB-\uDFFF])\u200D\uD83E\uDD1D\u200D\uD83E\uDDD1)(?:\uD83C[\uDFFB-\uDFFF])|\uD83D\uDC69\u200D\uD83D\uDC69\u200D(?:\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67]))|\uD83D\uDC69(?:\u200D(?:\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D(?:\uD83D[\uDC68\uDC69])|\uD83D[\uDC68\uDC69])|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFF\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFE\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFD\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFC\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFB\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD]))|\uD83E\uDDD1(?:\u200D(?:\uD83E\uDD1D\u200D\uD83E\uDDD1|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFF\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFE\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFD\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFC\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFB\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD]))|\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC69\u200D\uD83D\uDC69\u200D(?:\uD83D[\uDC66\uDC67])|\uD83D\uDC69\u200D\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67])|(?:\uD83D\uDC41\uFE0F\u200D\uD83D\uDDE8|\uD83E\uDDD1(?:\uD83C\uDFFF\u200D[\u2695\u2696\u2708]|\uD83C\uDFFE\u200D[\u2695\u2696\u2708]|\uD83C\uDFFD\u200D[\u2695\u2696\u2708]|\uD83C\uDFFC\u200D[\u2695\u2696\u2708]|\uD83C\uDFFB\u200D[\u2695\u2696\u2708]|\u200D[\u2695\u2696\u2708])|\uD83D\uDC69(?:\uD83C\uDFFF\u200D[\u2695\u2696\u2708]|\uD83C\uDFFE\u200D[\u2695\u2696\u2708]|\uD83C\uDFFD\u200D[\u2695\u2696\u2708]|\uD83C\uDFFC\u200D[\u2695\u2696\u2708]|\uD83C\uDFFB\u200D[\u2695\u2696\u2708]|\u200D[\u2695\u2696\u2708])|\uD83D\uDE36\u200D\uD83C\uDF2B|\uD83C\uDFF3\uFE0F\u200D\u26A7|\uD83D\uDC3B\u200D\u2744|(?:(?:\uD83C[\uDFC3\uDFC4\uDFCA]|\uD83D[\uDC6E\uDC70\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6]|\uD83E[\uDD26\uDD35\uDD37-\uDD39\uDD3D\uDD3E\uDDB8\uDDB9\uDDCD-\uDDCF\uDDD4\uDDD6-\uDDDD])(?:\uD83C[\uDFFB-\uDFFF])|\uD83D\uDC6F|\uD83E[\uDD3C\uDDDE\uDDDF])\u200D[\u2640\u2642]|(?:\u26F9|\uD83C[\uDFCB\uDFCC]|\uD83D\uDD75)(?:\uFE0F|\uD83C[\uDFFB-\uDFFF])\u200D[\u2640\u2642]|\uD83C\uDFF4\u200D\u2620|(?:\uD83C[\uDFC3\uDFC4\uDFCA]|\uD83D[\uDC6E\uDC70\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6]|\uD83E[\uDD26\uDD35\uDD37-\uDD39\uDD3D\uDD3E\uDDB8\uDDB9\uDDCD-\uDDCF\uDDD4\uDDD6-\uDDDD])\u200D[\u2640\u2642]|[\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u2328\u23CF\u23ED-\u23EF\u23F1\u23F2\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB\u25FC\u2600-\u2604\u260E\u2611\u2618\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u265F\u2660\u2663\u2665\u2666\u2668\u267B\u267E\u2692\u2694-\u2697\u2699\u269B\u269C\u26A0\u26A7\u26B0\u26B1\u26C8\u26CF\u26D1\u26D3\u26E9\u26F0\u26F1\u26F4\u26F7\u26F8\u2702\u2708\u2709\u270F\u2712\u2714\u2716\u271D\u2721\u2733\u2734\u2744\u2747\u2763\u27A1\u2934\u2935\u2B05-\u2B07\u3030\u303D\u3297\u3299]|\uD83C[\uDD70\uDD71\uDD7E\uDD7F\uDE02\uDE37\uDF21\uDF24-\uDF2C\uDF36\uDF7D\uDF96\uDF97\uDF99-\uDF9B\uDF9E\uDF9F\uDFCD\uDFCE\uDFD4-\uDFDF\uDFF5\uDFF7]|\uD83D[\uDC3F\uDCFD\uDD49\uDD4A\uDD6F\uDD70\uDD73\uDD76-\uDD79\uDD87\uDD8A-\uDD8D\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA\uDECB\uDECD-\uDECF\uDEE0-\uDEE5\uDEE9\uDEF0\uDEF3])\uFE0F|\uD83C\uDFF3\uFE0F\u200D\uD83C\uDF08|\uD83D\uDC69\u200D\uD83D\uDC67|\uD83D\uDC69\u200D\uD83D\uDC66|\uD83D\uDE35\u200D\uD83D\uDCAB|\uD83D\uDE2E\u200D\uD83D\uDCA8|\uD83D\uDC15\u200D\uD83E\uDDBA|\uD83E\uDDD1(?:\uD83C\uDFFF|\uD83C\uDFFE|\uD83C\uDFFD|\uD83C\uDFFC|\uD83C\uDFFB)?|\uD83D\uDC69(?:\uD83C\uDFFF|\uD83C\uDFFE|\uD83C\uDFFD|\uD83C\uDFFC|\uD83C\uDFFB)?|\uD83C\uDDFD\uD83C\uDDF0|\uD83C\uDDF6\uD83C\uDDE6|\uD83C\uDDF4\uD83C\uDDF2|\uD83D\uDC08\u200D\u2B1B|\u2764\uFE0F\u200D(?:\uD83D\uDD25|\uD83E\uDE79)|\uD83D\uDC41\uFE0F|\uD83C\uDFF3\uFE0F|\uD83C\uDDFF(?:\uD83C[\uDDE6\uDDF2\uDDFC])|\uD83C\uDDFE(?:\uD83C[\uDDEA\uDDF9])|\uD83C\uDDFC(?:\uD83C[\uDDEB\uDDF8])|\uD83C\uDDFB(?:\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDEE\uDDF3\uDDFA])|\uD83C\uDDFA(?:\uD83C[\uDDE6\uDDEC\uDDF2\uDDF3\uDDF8\uDDFE\uDDFF])|\uD83C\uDDF9(?:\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDED\uDDEF-\uDDF4\uDDF7\uDDF9\uDDFB\uDDFC\uDDFF])|\uD83C\uDDF8(?:\uD83C[\uDDE6-\uDDEA\uDDEC-\uDDF4\uDDF7-\uDDF9\uDDFB\uDDFD-\uDDFF])|\uD83C\uDDF7(?:\uD83C[\uDDEA\uDDF4\uDDF8\uDDFA\uDDFC])|\uD83C\uDDF5(?:\uD83C[\uDDE6\uDDEA-\uDDED\uDDF0-\uDDF3\uDDF7-\uDDF9\uDDFC\uDDFE])|\uD83C\uDDF3(?:\uD83C[\uDDE6\uDDE8\uDDEA-\uDDEC\uDDEE\uDDF1\uDDF4\uDDF5\uDDF7\uDDFA\uDDFF])|\uD83C\uDDF2(?:\uD83C[\uDDE6\uDDE8-\uDDED\uDDF0-\uDDFF])|\uD83C\uDDF1(?:\uD83C[\uDDE6-\uDDE8\uDDEE\uDDF0\uDDF7-\uDDFB\uDDFE])|\uD83C\uDDF0(?:\uD83C[\uDDEA\uDDEC-\uDDEE\uDDF2\uDDF3\uDDF5\uDDF7\uDDFC\uDDFE\uDDFF])|\uD83C\uDDEF(?:\uD83C[\uDDEA\uDDF2\uDDF4\uDDF5])|\uD83C\uDDEE(?:\uD83C[\uDDE8-\uDDEA\uDDF1-\uDDF4\uDDF6-\uDDF9])|\uD83C\uDDED(?:\uD83C[\uDDF0\uDDF2\uDDF3\uDDF7\uDDF9\uDDFA])|\uD83C\uDDEC(?:\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEE\uDDF1-\uDDF3\uDDF5-\uDDFA\uDDFC\uDDFE])|\uD83C\uDDEB(?:\uD83C[\uDDEE-\uDDF0\uDDF2\uDDF4\uDDF7])|\uD83C\uDDEA(?:\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDED\uDDF7-\uDDFA])|\uD83C\uDDE9(?:\uD83C[\uDDEA\uDDEC\uDDEF\uDDF0\uDDF2\uDDF4\uDDFF])|\uD83C\uDDE8(?:\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDEE\uDDF0-\uDDF5\uDDF7\uDDFA-\uDDFF])|\uD83C\uDDE7(?:\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEF\uDDF1-\uDDF4\uDDF6-\uDDF9\uDDFB\uDDFC\uDDFE\uDDFF])|\uD83C\uDDE6(?:\uD83C[\uDDE8-\uDDEC\uDDEE\uDDF1\uDDF2\uDDF4\uDDF6-\uDDFA\uDDFC\uDDFD\uDDFF])|[#\*0-9]\uFE0F\u20E3|\u2764\uFE0F|(?:\uD83C[\uDFC3\uDFC4\uDFCA]|\uD83D[\uDC6E\uDC70\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6]|\uD83E[\uDD26\uDD35\uDD37-\uDD39\uDD3D\uDD3E\uDDB8\uDDB9\uDDCD-\uDDCF\uDDD4\uDDD6-\uDDDD])(?:\uD83C[\uDFFB-\uDFFF])|(?:\u26F9|\uD83C[\uDFCB\uDFCC]|\uD83D\uDD75)(?:\uFE0F|\uD83C[\uDFFB-\uDFFF])|\uD83C\uDFF4|(?:[\u270A\u270B]|\uD83C[\uDF85\uDFC2\uDFC7]|\uD83D[\uDC42\uDC43\uDC46-\uDC50\uDC66\uDC67\uDC6B-\uDC6D\uDC72\uDC74-\uDC76\uDC78\uDC7C\uDC83\uDC85\uDC8F\uDC91\uDCAA\uDD7A\uDD95\uDD96\uDE4C\uDE4F\uDEC0\uDECC]|\uD83E[\uDD0C\uDD0F\uDD18-\uDD1C\uDD1E\uDD1F\uDD30-\uDD34\uDD36\uDD77\uDDB5\uDDB6\uDDBB\uDDD2\uDDD3\uDDD5])(?:\uD83C[\uDFFB-\uDFFF])|(?:[\u261D\u270C\u270D]|\uD83D[\uDD74\uDD90])(?:\uFE0F|\uD83C[\uDFFB-\uDFFF])|[\u270A\u270B]|\uD83C[\uDF85\uDFC2\uDFC7]|\uD83D[\uDC08\uDC15\uDC3B\uDC42\uDC43\uDC46-\uDC50\uDC66\uDC67\uDC6B-\uDC6D\uDC72\uDC74-\uDC76\uDC78\uDC7C\uDC83\uDC85\uDC8F\uDC91\uDCAA\uDD7A\uDD95\uDD96\uDE2E\uDE35\uDE36\uDE4C\uDE4F\uDEC0\uDECC]|\uD83E[\uDD0C\uDD0F\uDD18-\uDD1C\uDD1E\uDD1F\uDD30-\uDD34\uDD36\uDD77\uDDB5\uDDB6\uDDBB\uDDD2\uDDD3\uDDD5]|\uD83C[\uDFC3\uDFC4\uDFCA]|\uD83D[\uDC6E\uDC70\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6]|\uD83E[\uDD26\uDD35\uDD37-\uDD39\uDD3D\uDD3E\uDDB8\uDDB9\uDDCD-\uDDCF\uDDD4\uDDD6-\uDDDD]|\uD83D\uDC6F|\uD83E[\uDD3C\uDDDE\uDDDF]|[\u231A\u231B\u23E9-\u23EC\u23F0\u23F3\u25FD\u25FE\u2614\u2615\u2648-\u2653\u267F\u2693\u26A1\u26AA\u26AB\u26BD\u26BE\u26C4\u26C5\u26CE\u26D4\u26EA\u26F2\u26F3\u26F5\u26FA\u26FD\u2705\u2728\u274C\u274E\u2753-\u2755\u2757\u2795-\u2797\u27B0\u27BF\u2B1B\u2B1C\u2B50\u2B55]|\uD83C[\uDC04\uDCCF\uDD8E\uDD91-\uDD9A\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF7C\uDF7E-\uDF84\uDF86-\uDF93\uDFA0-\uDFC1\uDFC5\uDFC6\uDFC8\uDFC9\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF8-\uDFFF]|\uD83D[\uDC00-\uDC07\uDC09-\uDC14\uDC16-\uDC3A\uDC3C-\uDC3E\uDC40\uDC44\uDC45\uDC51-\uDC65\uDC6A\uDC79-\uDC7B\uDC7D-\uDC80\uDC84\uDC88-\uDC8E\uDC90\uDC92-\uDCA9\uDCAB-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDDA4\uDDFB-\uDE2D\uDE2F-\uDE34\uDE37-\uDE44\uDE48-\uDE4A\uDE80-\uDEA2\uDEA4-\uDEB3\uDEB7-\uDEBF\uDEC1-\uDEC5\uDED0-\uDED2\uDED5-\uDED7\uDEEB\uDEEC\uDEF4-\uDEFC\uDFE0-\uDFEB]|\uD83E[\uDD0D\uDD0E\uDD10-\uDD17\uDD1D\uDD20-\uDD25\uDD27-\uDD2F\uDD3A\uDD3F-\uDD45\uDD47-\uDD76\uDD78\uDD7A-\uDDB4\uDDB7\uDDBA\uDDBC-\uDDCB\uDDD0\uDDE0-\uDDFF\uDE70-\uDE74\uDE78-\uDE7A\uDE80-\uDE86\uDE90-\uDEA8\uDEB0-\uDEB6\uDEC0-\uDEC2\uDED0-\uDED6]|(?:[\u231A\u231B\u23E9-\u23EC\u23F0\u23F3\u25FD\u25FE\u2614\u2615\u2648-\u2653\u267F\u2693\u26A1\u26AA\u26AB\u26BD\u26BE\u26C4\u26C5\u26CE\u26D4\u26EA\u26F2\u26F3\u26F5\u26FA\u26FD\u2705\u270A\u270B\u2728\u274C\u274E\u2753-\u2755\u2757\u2795-\u2797\u27B0\u27BF\u2B1B\u2B1C\u2B50\u2B55]|\uD83C[\uDC04\uDCCF\uDD8E\uDD91-\uDD9A\uDDE6-\uDDFF\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF7C\uDF7E-\uDF93\uDFA0-\uDFCA\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF4\uDFF8-\uDFFF]|\uD83D[\uDC00-\uDC3E\uDC40\uDC42-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDD7A\uDD95\uDD96\uDDA4\uDDFB-\uDE4F\uDE80-\uDEC5\uDECC\uDED0-\uDED2\uDED5-\uDED7\uDEEB\uDEEC\uDEF4-\uDEFC\uDFE0-\uDFEB]|\uD83E[\uDD0C-\uDD3A\uDD3C-\uDD45\uDD47-\uDD78\uDD7A-\uDDCB\uDDCD-\uDDFF\uDE70-\uDE74\uDE78-\uDE7A\uDE80-\uDE86\uDE90-\uDEA8\uDEB0-\uDEB6\uDEC0-\uDEC2\uDED0-\uDED6])|(?:[#\*0-9\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u231A\u231B\u2328\u23CF\u23E9-\u23F3\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB-\u25FE\u2600-\u2604\u260E\u2611\u2614\u2615\u2618\u261D\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u2648-\u2653\u265F\u2660\u2663\u2665\u2666\u2668\u267B\u267E\u267F\u2692-\u2697\u2699\u269B\u269C\u26A0\u26A1\u26A7\u26AA\u26AB\u26B0\u26B1\u26BD\u26BE\u26C4\u26C5\u26C8\u26CE\u26CF\u26D1\u26D3\u26D4\u26E9\u26EA\u26F0-\u26F5\u26F7-\u26FA\u26FD\u2702\u2705\u2708-\u270D\u270F\u2712\u2714\u2716\u271D\u2721\u2728\u2733\u2734\u2744\u2747\u274C\u274E\u2753-\u2755\u2757\u2763\u2764\u2795-\u2797\u27A1\u27B0\u27BF\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B50\u2B55\u3030\u303D\u3297\u3299]|\uD83C[\uDC04\uDCCF\uDD70\uDD71\uDD7E\uDD7F\uDD8E\uDD91-\uDD9A\uDDE6-\uDDFF\uDE01\uDE02\uDE1A\uDE2F\uDE32-\uDE3A\uDE50\uDE51\uDF00-\uDF21\uDF24-\uDF93\uDF96\uDF97\uDF99-\uDF9B\uDF9E-\uDFF0\uDFF3-\uDFF5\uDFF7-\uDFFF]|\uD83D[\uDC00-\uDCFD\uDCFF-\uDD3D\uDD49-\uDD4E\uDD50-\uDD67\uDD6F\uDD70\uDD73-\uDD7A\uDD87\uDD8A-\uDD8D\uDD90\uDD95\uDD96\uDDA4\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA-\uDE4F\uDE80-\uDEC5\uDECB-\uDED2\uDED5-\uDED7\uDEE0-\uDEE5\uDEE9\uDEEB\uDEEC\uDEF0\uDEF3-\uDEFC\uDFE0-\uDFEB]|\uD83E[\uDD0C-\uDD3A\uDD3C-\uDD45\uDD47-\uDD78\uDD7A-\uDDCB\uDDCD-\uDDFF\uDE70-\uDE74\uDE78-\uDE7A\uDE80-\uDE86\uDE90-\uDEA8\uDEB0-\uDEB6\uDEC0-\uDEC2\uDED0-\uDED6])\uFE0F|(?:[\u261D\u26F9\u270A-\u270D]|\uD83C[\uDF85\uDFC2-\uDFC4\uDFC7\uDFCA-\uDFCC]|\uD83D[\uDC42\uDC43\uDC46-\uDC50\uDC66-\uDC78\uDC7C\uDC81-\uDC83\uDC85-\uDC87\uDC8F\uDC91\uDCAA\uDD74\uDD75\uDD7A\uDD90\uDD95\uDD96\uDE45-\uDE47\uDE4B-\uDE4F\uDEA3\uDEB4-\uDEB6\uDEC0\uDECC]|\uD83E[\uDD0C\uDD0F\uDD18-\uDD1F\uDD26\uDD30-\uDD39\uDD3C-\uDD3E\uDD77\uDDB5\uDDB6\uDDB8\uDDB9\uDDBB\uDDCD-\uDDCF\uDDD1-\uDDDD])/g},Ab=d1(Ob);function v(x,$={}){if(typeof x!="string"||x.length===0||($={ambiguousIsNarrow:!0,...$},x=v1(x),x.length===0))return 0;x=x.replace(Ab()," ");let B=$.ambiguousIsNarrow?1:2,K=0;for(let S of x){let Y=S.codePointAt(0);if(Y<=31||Y>=127&&Y<=159||Y>=768&&Y<=879)continue;switch(Vb.eastAsianWidth(S)){case"F":case"W":K+=2;break;case"A":K+=B;break;default:K+=1}}return K}var $1=10,L1=(x=0)=>($)=>`\x1B[${$+x}m`,_1=(x=0)=>($)=>`\x1B[${38+x};5;${$}m`,U1=(x=0)=>($,B,K)=>`\x1B[${38+x};2;${$};${B};${K}m`,G={modifier:{reset:[0,0],bold:[1,22],dim:[2,22],italic:[3,23],underline:[4,24],overline:[53,55],inverse:[7,27],hidden:[8,28],strikethrough:[9,29]},color:{black:[30,39],red:[31,39],green:[32,39],yellow:[33,39],blue:[34,39],magenta:[35,39],cyan:[36,39],white:[37,39],blackBright:[90,39],gray:[90,39],grey:[90,39],redBright:[91,39],greenBright:[92,39],yellowBright:[93,39],blueBright:[94,39],magentaBright:[95,39],cyanBright:[96,39],whiteBright:[97,39]},bgColor:{bgBlack:[40,49],bgRed:[41,49],bgGreen:[42,49],bgYellow:[43,49],bgBlue:[44,49],bgMagenta:[45,49],bgCyan:[46,49],bgWhite:[47,49],bgBlackBright:[100,49],bgGray:[100,49],bgGrey:[100,49],bgRedBright:[101,49],bgGreenBright:[102,49],bgYellowBright:[103,49],bgBlueBright:[104,49],bgMagentaBright:[105,49],bgCyanBright:[106,49],bgWhiteBright:[107,49]}};Object.keys(G.modifier);var Mb=Object.keys(G.color),Nb=Object.keys(G.bgColor);[...Mb,...Nb];function Pb(){let x=new Map;for(let[$,B]of Object.entries(G)){for(let[K,S]of Object.entries(B))G[K]={open:`\x1B[${S[0]}m`,close:`\x1B[${S[1]}m`},B[K]=G[K],x.set(S[0],S[1]);Object.defineProperty(G,$,{value:B,enumerable:!1})}return Object.defineProperty(G,"codes",{value:x,enumerable:!1}),G.color.close="\x1B[39m",G.bgColor.close="\x1B[49m",G.color.ansi=L1(),G.color.ansi256=_1(),G.color.ansi16m=U1(),G.bgColor.ansi=L1($1),G.bgColor.ansi256=_1($1),G.bgColor.ansi16m=U1($1),Object.defineProperties(G,{rgbToAnsi256:{value:($,B,K)=>$===B&&B===K?$<8?16:$>248?231:Math.round(($-8)/247*24)+232:16+36*Math.round($/255*5)+6*Math.round(B/255*5)+Math.round(K/255*5),enumerable:!1},hexToRgb:{value:($)=>{let B=/[a-f\d]{6}|[a-f\d]{3}/i.exec($.toString(16));if(!B)return[0,0,0];let[K]=B;K.length===3&&(K=[...K].map((Y)=>Y+Y).join(""));let S=Number.parseInt(K,16);return[S>>16&255,S>>8&255,S&255]},enumerable:!1},hexToAnsi256:{value:($)=>G.rgbToAnsi256(...G.hexToRgb($)),enumerable:!1},ansi256ToAnsi:{value:($)=>{if($<8)return 30+$;if($<16)return 90+($-8);let B,K,S;if($>=232)B=(($-232)*10+8)/255,K=B,S=B;else{$-=16;let X=$%36;B=Math.floor($/36)/5,K=Math.floor(X/6)/5,S=X%6/5}let Y=Math.max(B,K,S)*2;if(Y===0)return 30;let b=30+(Math.round(S)<<2|Math.round(K)<<1|Math.round(B));return Y===2&&(b+=60),b},enumerable:!1},rgbToAnsi:{value:($,B,K)=>G.ansi256ToAnsi(G.rgbToAnsi256($,B,K)),enumerable:!1},hexToAnsi:{value:($)=>G.ansi256ToAnsi(G.hexToAnsi256($)),enumerable:!1}}),G}var Rb=Pb(),o=new Set(["\x1B","\x9B"]),Tb=39,S1="\x07",p1="[",mb="]",h1="m",Y1=`${mb}8;;`,C1=(x)=>`${o.values().next().value}${p1}${x}${h1}`,j1=(x)=>`${o.values().next().value}${Y1}${x}${S1}`,Lb=(x)=>x.split(" ").map(($)=>v($)),B1=(x,$,B)=>{let K=[...$],S=!1,Y=!1,b=v(v1(x[x.length-1]));for(let[X,J]of K.entries()){let Z=v(J);if(b+Z<=B?x[x.length-1]+=J:(x.push(J),b=0),o.has(J)&&(S=!0,Y=K.slice(X+1).join("").startsWith(Y1)),S){Y?J===S1&&(S=!1,Y=!1):J===h1&&(S=!1);continue}b+=Z,b===B&&X0&&x.length>1&&(x[x.length-2]+=x.pop())},_b=(x)=>{let $=x.split(" "),B=$.length;for(;B>0&&!(v($[B-1])>0);)B--;return B===$.length?x:$.slice(0,B).join(" ")+$.slice(B).join("")},Ub=(x,$,B={})=>{if(B.trim!==!1&&x.trim()==="")return"";let K="",S,Y,b=Lb(x),X=[""];for(let[Z,z]of x.split(" ").entries()){B.trim!==!1&&(X[X.length-1]=X[X.length-1].trimStart());let q=v(X[X.length-1]);if(Z!==0&&(q>=$&&(B.wordWrap===!1||B.trim===!1)&&(X.push(""),q=0),(q>0||B.trim===!1)&&(X[X.length-1]+=" ",q++)),B.hard&&b[Z]>$){let H=$-q,N=1+Math.floor((b[Z]-H-1)/$);Math.floor((b[Z]-1)/$)$&&q>0&&b[Z]>0){if(B.wordWrap===!1&&q<$){B1(X,z,$);continue}X.push("")}if(q+b[Z]>$&&B.wordWrap===!1){B1(X,z,$);continue}X[X.length-1]+=z}B.trim!==!1&&(X=X.map((Z)=>_b(Z)));let J=[...X.join(` +`)];for(let[Z,z]of J.entries()){if(K+=z,o.has(z)){let{groups:H}=new RegExp(`(?:\\${p1}(?\\d+)m|\\${Y1}(?.*)${S1})`).exec(J.slice(Z).join(""))||{groups:{}};if(H.code!==void 0){let N=Number.parseFloat(H.code);S=N===Tb?void 0:N}else H.uri!==void 0&&(Y=H.uri.length===0?void 0:H.uri)}let q=Rb.codes.get(Number(S));J[Z+1]===` +`?(Y&&(K+=j1("")),S&&q&&(K+=C1(q))):z===` +`&&(S&&q&&(K+=C1(S)),Y&&(K+=j1(Y)))}return K};function k1(x,$,B){return String(x).normalize().replace(/\r\n/g,` +`).split(` +`).map((K)=>Ub(K,$,B)).join(` +`)}var Cb=["up","down","left","right","space","enter","cancel"],r={actions:new Set(Cb),aliases:new Map([["k","up"],["j","down"],["h","left"],["l","right"],["\x03","cancel"],["escape","cancel"]])};function Q1(x,$){if(typeof x=="string")return r.aliases.get(x)===$;for(let B of x)if(B!==void 0&&Q1(B,$))return!0;return!1}function jb(x,$){if(x===$)return;let B=x.split(` +`),K=$.split(` +`),S=[];for(let Y=0;Y{let Z=String(b);if(Q1([Z,X,J],"cancel")){K&&$.write(T.cursor.show),process.exit(0);return}if(!B)return;j.moveCursor($,X==="return"?0:-1,X==="return"?-1:0,()=>{j.clearLine($,1,()=>{x.once("keypress",Y)})})};return K&&$.write(T.cursor.hide),x.once("keypress",Y),()=>{x.off("keypress",Y),K&&$.write(T.cursor.show),x.isTTY&&!kb&&x.setRawMode(!1),S.terminal=!1,S.close()}}var yb=Object.defineProperty,Eb=(x,$,B)=>($ in x)?yb(x,$,{enumerable:!0,configurable:!0,writable:!0,value:B}):x[$]=B,_=(x,$,B)=>(Eb(x,typeof $!="symbol"?$+"":$,B),B);class a{constructor(x,$=!0){_(this,"input"),_(this,"output"),_(this,"_abortSignal"),_(this,"rl"),_(this,"opts"),_(this,"_render"),_(this,"_track",!1),_(this,"_prevFrame",""),_(this,"_subscribers",new Map),_(this,"_cursor",0),_(this,"state","initial"),_(this,"error",""),_(this,"value");let{input:B=E1,output:K=w1,render:S,signal:Y,...b}=x;this.opts=b,this.onKeypress=this.onKeypress.bind(this),this.close=this.close.bind(this),this.render=this.render.bind(this),this._render=S.bind(this),this._track=$,this._abortSignal=Y,this.input=B,this.output=K}unsubscribe(){this._subscribers.clear()}setSubscriber(x,$){let B=this._subscribers.get(x)??[];B.push($),this._subscribers.set(x,B)}on(x,$){this.setSubscriber(x,{cb:$})}once(x,$){this.setSubscriber(x,{cb:$,once:!0})}emit(x,...$){let B=this._subscribers.get(x)??[],K=[];for(let S of B)S.cb(...$),S.once&&K.push(()=>B.splice(B.indexOf(S),1));for(let S of K)S()}prompt(){return new Promise((x,$)=>{if(this._abortSignal){if(this._abortSignal.aborted)return this.state="cancel",this.close(),x(K1);this._abortSignal.addEventListener("abort",()=>{this.state="cancel",this.close()},{once:!0})}let B=new Wb(0);B._write=(K,S,Y)=>{this._track&&(this.value=this.rl?.line.replace(/\t/g,""),this._cursor=this.rl?.cursor??0,this.emit("value",this.value)),Y()},this.input.pipe(B),this.rl=m1.createInterface({input:this.input,output:B,tabSize:2,prompt:"",escapeCodeTimeout:50}),m1.emitKeypressEvents(this.input,this.rl),this.rl.prompt(),this.opts.initialValue!==void 0&&this._track&&this.rl.write(this.opts.initialValue),this.input.on("keypress",this.onKeypress),c(this.input,!0),this.output.on("resize",this.render),this.render(),this.once("submit",()=>{this.output.write(T.cursor.show),this.output.off("resize",this.render),c(this.input,!1),x(this.value)}),this.once("cancel",()=>{this.output.write(T.cursor.show),this.output.off("resize",this.render),c(this.input,!1),x(K1)})})}onKeypress(x,$){if(this.state==="error"&&(this.state="active"),$?.name&&(!this._track&&r.aliases.has($.name)&&this.emit("cursor",r.aliases.get($.name)),r.actions.has($.name)&&this.emit("cursor",$.name)),x&&(x.toLowerCase()==="y"||x.toLowerCase()==="n")&&this.emit("confirm",x.toLowerCase()==="y"),x==="\t"&&this.opts.placeholder&&(this.value||(this.rl?.write(this.opts.placeholder),this.emit("value",this.opts.placeholder))),x&&this.emit("key",x.toLowerCase()),$?.name==="return"){if(this.opts.validate){let B=this.opts.validate(this.value);B&&(this.error=B instanceof Error?B.message:B,this.state="error",this.rl?.write(this.value))}this.state!=="error"&&(this.state="submit")}Q1([x,$?.name,$?.sequence],"cancel")&&(this.state="cancel"),(this.state==="submit"||this.state==="cancel")&&this.emit("finalize"),this.render(),(this.state==="submit"||this.state==="cancel")&&this.close()}close(){this.input.unpipe(),this.input.removeListener("keypress",this.onKeypress),this.output.write(` +`),c(this.input,!1),this.rl?.close(),this.rl=void 0,this.emit(`${this.state}`,this.value),this.unsubscribe()}restoreCursor(){let x=k1(this._prevFrame,process.stdout.columns,{hard:!0}).split(` +`).length-1;this.output.write(T.cursor.move(-999,x*-1))}render(){let x=k1(this._render(this)??"",process.stdout.columns,{hard:!0});if(x!==this._prevFrame){if(this.state==="initial")this.output.write(T.cursor.hide);else{let $=jb(this._prevFrame,x);if(this.restoreCursor(),$&&$?.length===1){let B=$[0];this.output.write(T.cursor.move(0,B)),this.output.write(T.erase.lines(1));let K=x.split(` +`);this.output.write(K[B]),this._prevFrame=x,this.output.write(T.cursor.move(0,K.length-B-1));return}if($&&$?.length>1){let B=$[0];this.output.write(T.cursor.move(0,B)),this.output.write(T.erase.down());let K=x.split(` +`).slice(B);this.output.write(K.join(` +`)),this._prevFrame=x;return}this.output.write(T.erase.down())}this.output.write(x),this.state==="initial"&&(this.state="active"),this._prevFrame=x}}}class X1 extends a{get cursor(){return this.value?0:1}get _value(){return this.cursor===0}constructor(x){super(x,!1),this.value=!!x.initialValue,this.on("value",()=>{this.value=this._value}),this.on("confirm",($)=>{this.output.write(T.cursor.move(0,-1)),this.value=$,this.state="submit",this.close()}),this.on("cursor",()=>{this.value=!this.value})}}var wb=Object.defineProperty,fb=(x,$,B)=>($ in x)?wb(x,$,{enumerable:!0,configurable:!0,writable:!0,value:B}):x[$]=B,y1=(x,$,B)=>(fb(x,typeof $!="symbol"?$+"":$,B),B);class J1 extends a{constructor(x){super(x,!1),y1(this,"options"),y1(this,"cursor",0),this.options=x.options,this.cursor=this.options.findIndex(({value:$})=>$===x.initialValue),this.cursor===-1&&(this.cursor=0),this.changeValue(),this.on("cursor",($)=>{switch($){case"left":case"up":this.cursor=this.cursor===0?this.options.length-1:this.cursor-1;break;case"down":case"right":this.cursor=this.cursor===this.options.length-1?0:this.cursor+1;break}this.changeValue()})}get _value(){return this.options[this.cursor]}changeValue(){this.value=this._value.value}}class Z1 extends a{get valueWithCursor(){if(this.state==="submit")return this.value;if(this.cursor>=this.value.length)return`${this.value}\u2588`;let x=this.value.slice(0,this.cursor),[$,...B]=this.value.slice(this.cursor);return`${x}${f1.default.inverse($)}${B.join("")}`}get cursor(){return this._cursor}constructor(x){super(x),this.on("finalize",()=>{this.value||(this.value=x.defaultValue)})}}var Q=h(x1(),1),n=h(e(),1);import C from"process";function vb(){return C.platform!=="win32"?C.env.TERM!=="linux":!!C.env.CI||!!C.env.WT_SESSION||!!C.env.TERMINUS_SUBLIME||C.env.ConEmuTask==="{cmd::Cmder}"||C.env.TERM_PROGRAM==="Terminus-Sublime"||C.env.TERM_PROGRAM==="vscode"||C.env.TERM==="xterm-256color"||C.env.TERM==="alacritty"||C.env.TERMINAL_EMULATOR==="JetBrains-JediTerm"}var z1=vb(),I=(x,$)=>z1?x:$,db=I("\u25C6","*"),c1=I("\u25A0","x"),r1=I("\u25B2","x"),u=I("\u25C7","o"),gb=I("\u250C","T"),W=I("\u2502","|"),w=I("\u2514","\u2014"),W1=I("\u25CF",">"),G1=I("\u25CB"," "),fx=I("\u25FB","[\u2022]"),vx=I("\u25FC","[+]"),dx=I("\u25FB","[ ]"),gx=I("\u25AA","\u2022"),F1=I("\u2500","-"),pb=I("\u256E","+"),hb=I("\u251C","+"),lb=I("\u256F","+"),Fb=I("\u25CF","\u2022"),cb=I("\u25C6","*"),rb=I("\u25B2","!"),ob=I("\u25A0","x"),I1=(x)=>{switch(x){case"initial":case"active":return Q.default.cyan(db);case"cancel":return Q.default.red(c1);case"error":return Q.default.yellow(r1);case"submit":return Q.default.green(u)}},ab=(x)=>{let{cursor:$,options:B,style:K}=x,S=x.maxItems??Number.POSITIVE_INFINITY,Y=Math.max(process.stdout.rows-4,0),b=Math.min(Y,Math.max(S,5)),X=0;$>=X+b-3?X=Math.max(Math.min($-b+3,B.length-b),0):$0,Z=b{let N=q===0&&J,V=q===H.length-1&&Z;return N||V?Q.default.dim("..."):K(z,q+X===$)})},d=(x)=>new Z1({validate:x.validate,placeholder:x.placeholder,defaultValue:x.defaultValue,initialValue:x.initialValue,render(){let $=`${Q.default.gray(W)} +${I1(this.state)} ${x.message} +`,B=x.placeholder?Q.default.inverse(x.placeholder[0])+Q.default.dim(x.placeholder.slice(1)):Q.default.inverse(Q.default.hidden("_")),K=this.value?this.valueWithCursor:B;switch(this.state){case"error":return`${$.trim()} +${Q.default.yellow(W)} ${K} +${Q.default.yellow(w)} ${Q.default.yellow(this.error)} +`;case"submit":return`${$}${Q.default.gray(W)} ${Q.default.dim(this.value||x.placeholder)}`;case"cancel":return`${$}${Q.default.gray(W)} ${Q.default.strikethrough(Q.default.dim(this.value??""))}${this.value?.trim()?` +${Q.default.gray(W)}`:""}`;default:return`${$}${Q.default.cyan(W)} ${K} +${Q.default.cyan(w)} +`}}}).prompt();var k=(x)=>{let $=x.active??"Yes",B=x.inactive??"No";return new X1({active:$,inactive:B,initialValue:x.initialValue??!0,render(){let K=`${Q.default.gray(W)} +${I1(this.state)} ${x.message} +`,S=this.value?$:B;switch(this.state){case"submit":return`${K}${Q.default.gray(W)} ${Q.default.dim(S)}`;case"cancel":return`${K}${Q.default.gray(W)} ${Q.default.strikethrough(Q.default.dim(S))} +${Q.default.gray(W)}`;default:return`${K}${Q.default.cyan(W)} ${this.value?`${Q.default.green(W1)} ${$}`:`${Q.default.dim(G1)} ${Q.default.dim($)}`} ${Q.default.dim("/")} ${this.value?`${Q.default.dim(G1)} ${Q.default.dim(B)}`:`${Q.default.green(W1)} ${B}`} +${Q.default.cyan(w)} +`}}}).prompt()},g=(x)=>{let $=(B,K)=>{let S=B.label??String(B.value);switch(K){case"selected":return`${Q.default.dim(S)}`;case"active":return`${Q.default.green(W1)} ${S} ${B.hint?Q.default.dim(`(${B.hint})`):""}`;case"cancelled":return`${Q.default.strikethrough(Q.default.dim(S))}`;default:return`${Q.default.dim(G1)} ${Q.default.dim(S)}`}};return new J1({options:x.options,initialValue:x.initialValue,render(){let B=`${Q.default.gray(W)} +${I1(this.state)} ${x.message} +`;switch(this.state){case"submit":return`${B}${Q.default.gray(W)} ${$(this.options[this.cursor],"selected")}`;case"cancel":return`${B}${Q.default.gray(W)} ${$(this.options[this.cursor],"cancelled")} +${Q.default.gray(W)}`;default:return`${B}${Q.default.cyan(W)} ${ab({cursor:this.cursor,options:this.options,maxItems:x.maxItems,style:(K,S)=>$(K,S?"active":"inactive")}).join(` +${Q.default.cyan(W)} `)} +${Q.default.cyan(w)} +`}}}).prompt()};var f=(x="",$="")=>{let B=` +${x} +`.split(` +`),K=q1($).length,S=Math.max(B.reduce((b,X)=>{let J=q1(X);return J.length>b?J.length:b},0),K)+2,Y=B.map((b)=>`${Q.default.gray(W)} ${Q.default.dim(b)}${" ".repeat(S-q1(b).length)}${Q.default.gray(W)}`).join(` +`);process.stdout.write(`${Q.default.gray(W)} +${Q.default.green(u)} ${Q.default.reset($)} ${Q.default.gray(F1.repeat(Math.max(S-K-1,1))+pb)} +${Y} +${Q.default.gray(hb+F1.repeat(S+2)+lb)} +`)},p=(x="")=>{process.stdout.write(`${Q.default.gray(w)} ${Q.default.red(x)} + +`)},H1=(x="")=>{process.stdout.write(`${Q.default.gray(gb)} ${x} +`)},o1=(x="")=>{process.stdout.write(`${Q.default.gray(W)} +${Q.default.gray(w)} ${x} + +`)},m={message:(x="",{symbol:$=Q.default.gray(W)}={})=>{let B=[`${Q.default.gray(W)}`];if(x){let[K,...S]=x.split(` +`);B.push(`${$} ${K}`,...S.map((Y)=>`${Q.default.gray(W)} ${Y}`))}process.stdout.write(`${B.join(` +`)} +`)},info:(x)=>{m.message(x,{symbol:Q.default.blue(Fb)})},success:(x)=>{m.message(x,{symbol:Q.default.green(cb)})},step:(x)=>{m.message(x,{symbol:Q.default.green(u)})},warn:(x)=>{m.message(x,{symbol:Q.default.yellow(rb)})},warning:(x)=>{m.warn(x)},error:(x)=>{m.message(x,{symbol:Q.default.red(ob)})}},a1=()=>{let x=z1?["\u25D2","\u25D0","\u25D3","\u25D1"]:["\u2022","o","O","0"],$=z1?80:120,B=process.env.CI==="true",K,S,Y=!1,b="",X,J=(P)=>{let R=P>1?"Something went wrong":"Canceled";Y&&M(R,P)},Z=()=>J(2),z=()=>J(1),q=()=>{process.on("uncaughtExceptionMonitor",Z),process.on("unhandledRejection",Z),process.on("SIGINT",z),process.on("SIGTERM",z),process.on("exit",J)},H=()=>{process.removeListener("uncaughtExceptionMonitor",Z),process.removeListener("unhandledRejection",Z),process.removeListener("SIGINT",z),process.removeListener("SIGTERM",z),process.removeListener("exit",J)},N=()=>{if(X===void 0)return;B&&process.stdout.write(` +`);let P=X.split(` +`);process.stdout.write(n.cursor.move(-999,P.length-1)),process.stdout.write(n.erase.down(P.length))},V=(P)=>P.replace(/\.+$/,""),U=(P="")=>{Y=!0,K=l1(),b=V(P),process.stdout.write(`${Q.default.gray(W)} +`);let R=0,O=0;q(),S=setInterval(()=>{if(B&&b===X)return;N(),X=b;let E=Q.default.magenta(x[R]),Bb=B?"...":".".repeat(Math.floor(O)).slice(0,3);process.stdout.write(`${E} ${b}${Bb}`),R=R+1{Y=!1,clearInterval(S),N();let O=R===0?Q.default.green(u):R===1?Q.default.red(c1):Q.default.red(r1);b=V(P??b),process.stdout.write(`${O} ${b} +`),H(),K()};return{start:U,stop:M,message:(P="")=>{b=V(P??b)}}},V1=async(x,$)=>{let B={},K=Object.keys(x);for(let S of K){let Y=x[S],b=await Y({results:B})?.catch((X)=>{throw X});if(typeof $?.onCancel=="function"&&y(b)){B[S]="canceled",$.onCancel({results:B});continue}B[S]=b}return B};import{readFile as t1,writeFile as i,mkdir as O1,access as D}from"fs/promises";import{execSync as D1,exec as ub}from"child_process";import{createHash as ib,randomBytes as tb}from"crypto";import{homedir as Db}from"os";import{join as A}from"path";var A1="0.1.0",sb=process.env.SEED_DIR||"/opt/seed",eb="https://raw.githubusercontent.com/seed-hypermedia/seed/main",s1=`${process.env.SEED_REPO_URL||eb}/ops/docker-compose.yml`,bx="https://notify.seed.hyper.media",xx="https://ln.seed.hyper.media",$x="https://ln.testnet.seed.hyper.media";function Bx(x=sb){return{seedDir:x,configPath:A(x,"config.json"),composePath:A(x,"docker-compose.yml"),deployLog:A(x,"deploy.log")}}function Kx(){return{run(x){return D1(x,{encoding:"utf-8",timeout:30000}).trim()},runSafe(x){try{return this.run(x)}catch{return null}},exec(x){return new Promise(($,B)=>{ub(x,{timeout:120000},(K,S,Y)=>{if(K)B(K);else $({stdout:S.toString().trim(),stderr:Y.toString().trim()})})})}}}function e1(x){switch(x){case"dev":return{testnet:!0,release_channel:"dev"};case"staging":return{testnet:!1,release_channel:"dev"};case"prod":default:return{testnet:!1,release_channel:"latest"}}}async function Sx(x){try{return await D(x.configPath),!0}catch{return!1}}async function Yx(x){let $=await t1(x.configPath,"utf-8");return JSON.parse($)}async function t(x,$){await O1($.seedDir,{recursive:!0}),await i($.configPath,JSON.stringify(x,null,2)+` +`,"utf-8")}function bb(x=10){let B=tb(x);return Array.from(B).map((K)=>"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"[K%62]).join("")}function L(x){let $=new Date().toISOString();if(!process.stdout.isTTY)console.log(`[${$}] ${x}`)}function Qx(x){let $=null,B=!1;try{let K=JSON.parse(x);for(let S of K){if(S.startsWith("SEED_LOG_LEVEL="))$=S.split("=")[1];if(S.startsWith("SEED_P2P_TESTNET_NAME=")&&S.split("=")[1])B=!0}}catch{}return{logLevel:$,testnet:B}}function Xx(x){let $=null,B=!1,K=!1;try{let S=JSON.parse(x);for(let Y of S){if(Y.startsWith("SEED_BASE_URL="))$=Y.split("=")[1];if(Y.startsWith("SEED_IS_GATEWAY=true"))B=!0;if(Y.startsWith("SEED_ENABLE_STATISTICS=true"))K=!0}}catch{}return{hostname:$,gateway:B,trafficStats:K}}function Jx(x){let $=x.split(":");return $.length>1?$[$.length-1]:"latest"}async function Zx(x){let $=Db(),B=[A($,".seed-site"),"/shm/gateway","/shm"],K=null;for(let M of B)try{await D(M),K=M;break}catch{}let S=x.runSafe("docker ps --format '{{.Names}}' 2>/dev/null | grep -q seed");if(!K&&S===null)return null;if(!K)K=A($,".seed-site");let Y=null,b=[A(K,"web","config.json"),"/shm/gateway/web/config.json",A($,".seed-site","web","config.json")];for(let M of b)try{let P=await t1(M,"utf-8"),R=JSON.parse(P);if(R.availableRegistrationSecret){Y=R.availableRegistrationSecret;break}}catch{}let X=null,J=null,Z=null,z=!1,q=!1,H=!1,N=x.runSafe("docker inspect seed-daemon --format '{{json .Config.Env}}' 2>/dev/null");if(N){let M=Qx(N);J=M.logLevel,z=M.testnet}let V=x.runSafe("docker inspect seed-web --format '{{json .Config.Env}}' 2>/dev/null");if(V){let M=Xx(V);X=M.hostname,q=M.gateway,H=M.trafficStats}let U=x.runSafe("docker inspect seed-web --format '{{.Config.Image}}' 2>/dev/null");if(U)Z=Jx(U);return{workspace:K,secret:Y,hostname:X,logLevel:J,imageTag:Z,testnet:z,gateway:q,trafficStats:H}}async function qx(x,$,B){H1(`Seed Node Migration v${A1}`),f([`Detected an existing Seed installation at: ${x.workspace}`,"","We'll import your current settings and migrate to the new deployment system.",`After migration, your node will be managed from ${$.seedDir}/ and updated via cron.`,"","Please review and confirm the detected values below."].join(` +`),"Existing installation found");let K=await V1({domain:()=>d({message:"Public hostname (including https://)",placeholder:"https://node1.seed.run",initialValue:x.hostname??"",validate:(V)=>{if(!V)return"Required";if(!V.startsWith("https://")&&!V.startsWith("http://"))return"Must start with https:// or http://"}}),email:()=>d({message:"Contact email \u2014 lets us notify you about security updates and node issues. Not shared publicly.",placeholder:"you@example.com",validate:(V)=>{if(!V)return"Required";if(!V.includes("@"))return"Must be a valid email"}}),environment:()=>g({message:"Environment",initialValue:x.testnet?"dev":"prod",options:[{value:"prod",label:"Production",hint:"stable releases, mainnet network \u2014 recommended"},{value:"staging",label:"Staging",hint:"development builds, mainnet network \u2014 for testing"},{value:"dev",label:"Development",hint:"development builds, testnet network"}]}),log_level:()=>g({message:"Log level",initialValue:x.logLevel??"info",options:[{value:"debug",label:"Debug",hint:"verbose, useful for troubleshooting"},{value:"info",label:"Info",hint:"standard operational logging"},{value:"warn",label:"Warn",hint:"only warnings and errors"},{value:"error",label:"Error",hint:"only errors"}]}),gateway:()=>k({message:"Run as public gateway?",initialValue:x.gateway}),analytics:()=>k({message:"Enable web analytics? Adds a Plausible.io dashboard to track your site's traffic.",initialValue:x.trafficStats})},{onCancel:()=>{p("Migration cancelled"),process.exit(0)}}),S=x.secret??bb();if(x.secret)m.success("Registration secret imported from existing installation.");else m.warn("No existing registration secret found. Generated a new one.");let Y=K.environment,b=e1(Y),X={domain:K.domain,email:K.email,compose_url:s1,compose_sha:"",compose_envs:{LOG_LEVEL:K.log_level},environment:Y,release_channel:b.release_channel,testnet:b.testnet,link_secret:S,analytics:K.analytics,gateway:K.gateway,last_script_run:""},J=Object.entries(X).map(([V,U])=>` ${V}: ${typeof U==="object"?JSON.stringify(U):U}`).join(` +`);f(J,"Configuration summary");let Z=await k({message:"Write config and proceed with deployment?"});if(y(Z)||!Z)p("Migration cancelled"),process.exit(0);await t(X,$),m.success(`Config written to ${$.configPath}`);let z=A(x.workspace,"web"),q=String(process.getuid()),H=String(process.getgid()),N=B.runSafe(`stat -c '%u:%g' "${z}" 2>/dev/null`);if(N&&N!==`${q}:${H}`){if(m.warn(`The web data directory (${z}) is owned by a different user (${N}). Updating ownership so the web container can write to it.`),!B.runSafe(`chown -R ${q}:${H} "${z}" 2>/dev/null`))B.runSafe(`sudo chown -R ${q}:${H} "${z}"`);m.success("File ownership updated.")}return X}async function zx(x){H1(`Seed Node Setup v${A1}`),f(["Welcome! This wizard will configure your new Seed node.","","Seed is a peer-to-peer hypermedia publishing system. This script sets up","the Docker containers, reverse proxy, and networking so your node is","reachable on the public internet.","",`Configuration will be saved to ${x.configPath}.`,"Subsequent runs of this script will deploy automatically (headless mode)."].join(` +`),"First-time setup");let $=await V1({domain:()=>d({message:"Public hostname (including https://)",placeholder:"https://node1.seed.run",validate:(J)=>{if(!J)return"Required";if(!J.startsWith("https://")&&!J.startsWith("http://"))return"Must start with https:// or http://"}}),email:()=>d({message:"Contact email \u2014 lets us notify you about security updates and node issues. Not shared publicly.",placeholder:"you@example.com",validate:(J)=>{if(!J)return"Required";if(!J.includes("@"))return"Must be a valid email"}}),environment:()=>g({message:"Environment",initialValue:"prod",options:[{value:"prod",label:"Production",hint:"stable releases, mainnet network \u2014 recommended"},{value:"staging",label:"Staging",hint:"development builds, mainnet network \u2014 for testing"},{value:"dev",label:"Development",hint:"development builds, testnet network"}]}),log_level:()=>g({message:"Log level for Seed services",initialValue:"info",options:[{value:"debug",label:"Debug",hint:"very verbose, useful for troubleshooting"},{value:"info",label:"Info",hint:"standard operational logging \u2014 recommended"},{value:"warn",label:"Warn",hint:"only warnings and errors"},{value:"error",label:"Error",hint:"only critical errors"}]}),gateway:()=>k({message:"Run as a public gateway? (serves all known public content)",initialValue:!1}),analytics:()=>k({message:"Enable web analytics? Adds a Plausible.io dashboard to track your site's traffic.",initialValue:!1})},{onCancel:()=>{p("Setup cancelled"),process.exit(0)}}),B=bb(),K=$.environment,S=e1(K),Y={domain:$.domain,email:$.email,compose_url:s1,compose_sha:"",compose_envs:{LOG_LEVEL:$.log_level},environment:K,release_channel:S.release_channel,testnet:S.testnet,link_secret:B,analytics:$.analytics,gateway:$.gateway,last_script_run:""},b=Object.entries(Y).filter(([J])=>J!=="compose_sha"&&J!=="last_script_run").map(([J,Z])=>` ${J}: ${typeof Z==="object"?JSON.stringify(Z):Z}`).join(` +`);f(b,"Configuration summary");let X=await k({message:"Write config and proceed with deployment?"});if(y(X)||!X)p("Setup cancelled"),process.exit(0);return await t(Y,x),m.success(`Config written to ${x.configPath}`),Y}function Wx(x){return x.replace(/^https?:\/\//,"").replace(/\/+$/,"")}function Gx(x){return`{$SEED_SITE_HOSTNAME} + +encode zstd gzip + +@ipfsget { + method GET HEAD OPTIONS + path /ipfs/* +} + +reverse_proxy /.metrics* grafana:{$SEED_SITE_MONITORING_PORT:3001} + +reverse_proxy @ipfsget seed-daemon:{$HM_SITE_BACKEND_GRPCWEB_PORT:56001} + +reverse_proxy * seed-web:{$SEED_SITE_LOCAL_PORT:3000} +`}function Ix(x){return ib("sha256").update(x).digest("hex")}async function n1(x){let $=["seed-proxy","seed-web","seed-daemon"];for(let B of $)if(x.runSafe(`docker inspect ${B} --format '{{.State.Running}}' 2>/dev/null`)!=="true")return!1;return!0}async function Hx(x){let $=new Map,B=["seed-proxy","seed-web","seed-daemon"];for(let K of B){let S=x.runSafe(`docker inspect ${K} --format '{{.Image}}' 2>/dev/null`);if(S)$.set(K,S)}return $}function xb(x,$){let B=Wx(x.domain),K=x.testnet?"dev":"",S=x.testnet?$x:xx,Y={SEED_SITE_HOSTNAME:x.domain,SEED_SITE_DNS:B,SEED_SITE_TAG:x.release_channel,SEED_SITE_WORKSPACE:$.seedDir,SEED_UID:String(process.getuid()),SEED_GID:String(process.getgid()),SEED_LOG_LEVEL:x.compose_envs.LOG_LEVEL,SEED_IS_GATEWAY:String(x.gateway),SEED_ENABLE_STATISTICS:String(x.analytics),SEED_P2P_TESTNET_NAME:K,SEED_LIGHTNING_URL:S,NOTIFY_SERVICE_HOST:bx,SEED_SITE_MONITORING_WORKDIR:A($.seedDir,"monitoring")};return Object.entries(Y).map(([b,X])=>`${b}="${X}"`).join(" ")}function Vx(x){return[A(x.seedDir,"proxy"),A(x.seedDir,"proxy","data"),A(x.seedDir,"proxy","config"),A(x.seedDir,"web"),A(x.seedDir,"daemon"),A(x.seedDir,"monitoring"),A(x.seedDir,"monitoring","grafana"),A(x.seedDir,"monitoring","prometheus")]}async function $b(x,$){try{await D(x.seedDir)}catch{try{await O1(x.seedDir,{recursive:!0})}catch{L(`Creating ${x.seedDir} requires elevated permissions`),$.run(`sudo mkdir -p "${x.seedDir}"`),$.run(`sudo chown "$(id -u):$(id -g)" "${x.seedDir}"`)}}}async function u1(x,$,B,K){L("Deployment failed \u2014 rolling back to previous images...");for(let[Y,b]of x)L(` Restoring ${Y} to image ${b.slice(0,16)}...`),K.runSafe(`docker stop ${Y} 2>/dev/null`),K.runSafe(`docker rm ${Y} 2>/dev/null`);L(" Running docker compose up with cached images...");let S=xb($,B);K.runSafe(`${S} docker compose -f ${B.composePath} up -d --quiet-pull 2>&1`),L("Rollback complete. Check container status with: docker ps")}async function i1(x,$,B){let K=process.stdout.isTTY,S=K?a1():null,Y=(O)=>{if(S)S.message(O);L(O)};if(K)m.step("Starting deployment...");S?.start("Fetching docker-compose.yml..."),Y("Fetching docker-compose.yml...");let b=process.env.SEED_REPO_URL,X=b?`${b}/ops/docker-compose.yml`:x.compose_url,J=await fetch(X);if(!J.ok)throw S?.stop("Failed to fetch docker-compose.yml"),Error(`Failed to fetch compose file from ${X}: ${J.status}`);let Z=await J.text(),z=Ix(Z),q=await n1(B);if(x.compose_sha===z&&q){S?.stop("No changes detected \u2014 all containers healthy. Skipping redeployment."),L("No changes detected \u2014 compose SHA matches and containers are healthy. Skipping."),x.last_script_run=new Date().toISOString(),await t(x,$);return}if(x.compose_sha&&x.compose_sha!==z)Y(`Compose file changed: ${x.compose_sha.slice(0,8)} -> ${z.slice(0,8)}`);await $b($,B),await i($.composePath,Z,"utf-8"),Y("Setting up workspace directories...");let H=Vx($);for(let O of H)await O1(O,{recursive:!0});Y("Generating Caddyfile...");let N=Gx(x);await i(A($.seedDir,"proxy","CaddyFile"),N,"utf-8");let V=A($.seedDir,"web","config.json"),U=!1;try{await D(V)}catch{U=!0,await i(V,JSON.stringify({availableRegistrationSecret:x.link_secret})+` +`,"utf-8"),Y("Created initial web/config.json with registration secret.")}Y("Stopping any existing containers..."),B.runSafe("docker stop seed-site seed-daemon seed-proxy grafana prometheus 2>/dev/null"),B.runSafe("docker rm seed-site seed-daemon seed-proxy grafana prometheus 2>/dev/null");let M=await Hx(B);Y("Running docker compose up...");let P=xb(x,$);try{let O=`${P} docker compose -f ${$.composePath} up -d --pull always --quiet-pull`,E=await B.exec(O);if(E.stderr)L(`compose stderr: ${E.stderr}`)}catch(O){if(S?.stop("docker compose up failed"),L(`docker compose up failed: ${O}`),M.size>0)await u1(M,x,$,B);throw Error(`Deployment failed: ${O}`)}Y("Running post-deploy health checks...");let R=!1;for(let O=0;O<10;O++){if(await new Promise((E)=>setTimeout(E,3000)),R=await n1(B),R)break;Y(`Health check attempt ${O+1}/10...`)}if(!R){if(S?.stop("Health checks failed"),L("Health checks failed \u2014 containers not running after 30s"),M.size>0)await u1(M,x,$,B);throw Error("Deployment failed: containers did not become healthy within 30 seconds")}if(x.compose_sha=z,x.last_script_run=new Date().toISOString(),await t(x,$),S?.stop("Deployment complete!"),L("Deployment complete."),K&&U)f([`Your site is live at ${x.domain}`,"",` Secret: ${x.link_secret}`,"","Open the Seed desktop app and enter this secret to link","your publisher account to this site."].join(` +`),"Setup complete")}async function Ox(x,$){let B=`0 2 * * * /usr/bin/bun ${A(x.seedDir,"deploy.js")} >> ${x.deployLog} 2>&1 # seed-deploy`,K=$.runSafe("crontab -l 2>/dev/null")??"";if(K.includes("seed-deploy")){L("Cron job already installed. Skipping.");return}let Y=[K,B,"0 0,4,8,12,16,20 * * * docker image prune -a -f # seed-cleanup"].filter(Boolean).join(` +`)+` +`;try{D1(`echo '${Y}' | crontab -`,{encoding:"utf-8"}),L("Installed nightly deployment cron job (02:00) and image cleanup cron.")}catch(b){L(`Warning: Failed to install cron job: ${b}`)}}async function Ax(){let x=Bx(),$=Kx();if(await $b(x,$),await Sx(x)){L(`Seed deploy v${A1} \u2014 config found at ${x.configPath}, running headless.`);let b=await Yx(x);await i1(b,x,$);return}let K=await Zx($),S;if(K)S=await qx(K,x,$);else S=await zx(x);let Y=await k({message:"Install nightly cron job for automatic updates? (runs at 02:00)",initialValue:!0});if(!y(Y)&&Y)await Ox(x,$),m.success("Cron job installed. Your node will auto-update nightly at 02:00.");await i1(S,x,$),o1("Setup complete! Your Seed node is running.")}if(import.meta.main)Ax().catch((x)=>{console.error("Fatal error:",x),process.exit(1)});export{t as writeConfig,Ix as sha256,Ox as setupCron,Yx as readConfig,Xx as parseWebEnv,Jx as parseImageTag,Qx as parseDaemonEnv,Kx as makeShellRunner,Bx as makePaths,L as log,Vx as getWorkspaceDirs,Hx as getContainerImages,bb as generateSecret,Gx as generateCaddyfile,Wx as extractDns,e1 as environmentPresets,$b as ensureSeedDir,Zx as detectOldInstall,i1 as deploy,Sx as configExists,n1 as checkContainersHealthy,xb as buildComposeEnv,A1 as VERSION,bx as NOTIFY_SERVICE_HOST,$x as LIGHTNING_URL_TESTNET,xx as LIGHTNING_URL_MAINNET,sb as DEFAULT_SEED_DIR,eb as DEFAULT_REPO_URL,s1 as DEFAULT_COMPOSE_URL}; diff --git a/docker-compose.yml b/ops/docker-compose.yml similarity index 99% rename from docker-compose.yml rename to ops/docker-compose.yml index dddad6c9a..825d2c9a3 100644 --- a/docker-compose.yml +++ b/ops/docker-compose.yml @@ -27,6 +27,7 @@ services: seed-web: container_name: seed-web image: seedhypermedia/web:${SEED_SITE_TAG:-latest} + user: "${SEED_UID}:${SEED_GID}" depends_on: - seed-daemon networks: diff --git a/ops/package.json b/ops/package.json new file mode 100644 index 000000000..8469627c6 --- /dev/null +++ b/ops/package.json @@ -0,0 +1,15 @@ +{ + "scripts": { + "dev": "bun run deploy.ts", + "build": "bun build deploy.ts --outfile dist/deploy.js --target bun --minify", + "typecheck": "tsc --noEmit", + "test": "bun test" + }, + "dependencies": { + "@clack/prompts": "^0.9.1" + }, + "devDependencies": { + "@types/bun": "^1.2.4", + "typescript": "^5.7.3" + } +} diff --git a/ops/tsconfig.json b/ops/tsconfig.json new file mode 100644 index 000000000..b20088b5b --- /dev/null +++ b/ops/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "types": ["bun-types"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "dist", + "rootDir": ".", + "declaration": false, + "noEmit": true + }, + "include": ["*.ts"], + "exclude": ["dist"] +} diff --git a/website_deployment.sh b/website_deployment.sh index ca0c74ec5..a45d28a50 100644 --- a/website_deployment.sh +++ b/website_deployment.sh @@ -1,4 +1,22 @@ #!/bin/sh +# ============================================================================= +# DEPRECATION NOTICE +# +# This script is deprecated and will be removed in a future release. +# Please use the new deployment system instead: +# +# curl -fsSL https://seed.hyper.media/deploy.sh | sudo sh +# +# The new system provides: +# - Interactive setup wizard with guided configuration +# - Automatic migration from existing installations +# - Nightly auto-updates via cron (no Watchtower dependency) +# - Idempotent headless mode for safe repeated runs +# - Config-driven deployments at /opt/seed/config.json +# +# See ops/deploy.ts in the seed repo for details. +# ============================================================================= + set -e command_exists() { @@ -81,7 +99,7 @@ hostname="${hostname%/}" mkdir -p ${workspace} rm -f ${workspace}/deployment.log touch ${workspace}/deployment.log -curl -s -o ${workspace}/hmsite.yml https://raw.githubusercontent.com/seed-hypermedia/seed/main/docker-compose.yml +curl -s -o ${workspace}/hmsite.yml https://raw.githubusercontent.com/seed-hypermedia/seed/main/ops/docker-compose.yml install_docker if [ -n "$profile" ]; then From 8f2829615e3fe01c893c33a2b5c3b96af1810255 Mon Sep 17 00:00:00 2001 From: juligasa <11684004+juligasa@users.noreply.github.com> Date: Mon, 16 Feb 2026 09:53:03 +0100 Subject: [PATCH 2/5] feat(ops): add SEED_BRANCH env var to deploy.sh for testing from non-main branches --- ops/deploy.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ops/deploy.sh b/ops/deploy.sh index 829213437..159e031da 100755 --- a/ops/deploy.sh +++ b/ops/deploy.sh @@ -10,7 +10,8 @@ set -e SEED_DIR="${SEED_DIR:-/opt/seed}" -SEED_REPO_URL="${SEED_REPO_URL:-https://raw.githubusercontent.com/seed-hypermedia/seed/main}" +SEED_BRANCH="${SEED_BRANCH:-main}" +GH_RAW="https://raw.githubusercontent.com/seed-hypermedia/seed/${SEED_BRANCH}/ops" command_exists() { command -v "$@" > /dev/null 2>&1 @@ -59,7 +60,7 @@ fi ensure_dir "${SEED_DIR}" info "Downloading deployment script..." -curl -fsSL "${SEED_REPO_URL}/ops/dist/deploy.js" -o "${SEED_DIR}/deploy.js" +curl -fsSL "${GH_RAW}/dist/deploy.js" -o "${SEED_DIR}/deploy.js" info "Running deployment script..." exec bun "${SEED_DIR}/deploy.js" From e1854a37c822afa2f1ed8255d1cfb894acffd9a3 Mon Sep 17 00:00:00 2001 From: juligasa <11684004+juligasa@users.noreply.github.com> Date: Mon, 16 Feb 2026 11:42:03 +0100 Subject: [PATCH 3/5] fix(ops): add glibc pre-flight check to deploy.sh Bun requires glibc >= 2.25 which excludes old distros like CentOS 7. Instead of swapping to Node.js (which would create a dev/prod runtime mismatch), we keep Bun everywhere and fail early with a clear error message listing minimum supported OS versions. --- ops/deploy.sh | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/ops/deploy.sh b/ops/deploy.sh index 159e031da..0d720673a 100755 --- a/ops/deploy.sh +++ b/ops/deploy.sh @@ -4,6 +4,11 @@ # Downloads the bundled deployment script and runs it with Bun. # Installs Docker and Bun only if they are not already present. # +# We use Bun as the production runtime (not Node.js) so that what developers +# test locally is exactly what runs on servers β€” one runtime, zero mismatch. +# Bun requires glibc >= 2.25, so older distros (CentOS 7, Amazon Linux 2, etc.) +# are not supported. The script checks this upfront and exits with a clear message. +# # Usage: # sh <(curl -fsSL https://raw.githubusercontent.com/seed-hypermedia/seed/main/ops/deploy.sh) @@ -12,6 +17,7 @@ set -e SEED_DIR="${SEED_DIR:-/opt/seed}" SEED_BRANCH="${SEED_BRANCH:-main}" GH_RAW="https://raw.githubusercontent.com/seed-hypermedia/seed/${SEED_BRANCH}/ops" +MIN_GLIBC="2.25" command_exists() { command -v "$@" > /dev/null 2>&1 @@ -33,6 +39,50 @@ ensure_dir() { fi } +# Compare two dotted version strings. Returns 0 (true) if $1 >= $2. +version_gte() { + # printf trick: pad each component to 3 digits, then compare lexicographically + local v1; v1=$(printf '%03d%03d' $(echo "$1" | tr '.' ' ')) + local v2; v2=$(printf '%03d%03d' $(echo "$2" | tr '.' ' ')) + [ "$v1" -ge "$v2" ] +} + +check_glibc() { + if ! command_exists ldd; then + info "Warning: Cannot determine glibc version (ldd not found). Proceeding anyway." + return + fi + + glibc_version=$(ldd --version 2>&1 | head -1 | grep -oE '[0-9]+\.[0-9]+$' || true) + if [ -z "$glibc_version" ]; then + info "Warning: Could not parse glibc version. Proceeding anyway." + return + fi + + if ! version_gte "$glibc_version" "$MIN_GLIBC"; then + cat >&2 <= $MIN_GLIBC to run. + +Minimum supported operating systems: + - Ubuntu 18.04+ + - Debian 10+ + - CentOS/RHEL 8+ + - Fedora 28+ + - Amazon Linux 2023+ + +Please upgrade your operating system and re-run this script. + +EOF + exit 1 + fi + + info "glibc $glibc_version detected (>= $MIN_GLIBC). OK." +} + +check_glibc + if ! command_exists docker; then info "Installing Docker (requires sudo)..." curl -fsSL https://get.docker.com -o /tmp/install-docker.sh @@ -49,7 +99,9 @@ if ! command_exists bun; then export BUN_INSTALL="${HOME}/.bun" export PATH="${BUN_INSTALL}/bin:${PATH}" if ! command_exists bun; then - echo "ERROR: Bun installation failed. Please install manually: https://bun.sh" >&2 + echo "ERROR: Bun installation failed." >&2 + echo "This may be a glibc compatibility issue. Bun requires glibc >= $MIN_GLIBC." >&2 + echo "Check your version with: ldd --version" >&2 exit 1 fi info "Bun installed: $(bun --version)" From a53c91fd6bf37918442d4cc08302dcc146f49f6c Mon Sep 17 00:00:00 2001 From: juligasa <11684004+juligasa@users.noreply.github.com> Date: Tue, 17 Feb 2026 19:00:56 +0100 Subject: [PATCH 4/5] feat(ops): full CLI, reconfigure wizard, docker-compose hardening Major additions to the deployment system: CLI: add deploy, stop, start, restart, status, config, logs, cron, backup, restore, uninstall commands with --help and --version flags. Add --reconfigure flag to re-run the setup wizard with existing values shown as placeholders (Tab to keep, Enter to clear/skip). Wizard: move email to last question and make it optional. Use dynamic placeholders instead of initialValue for text fields so Tab accepts and Enter clears. Configuration summary shows only user-facing fields with pencil icon on changed fields during reconfigure. Status: unicode indicators (checkmark/warning/cross), monitoring section only shown when metrics profile is active, reconfigure hint on no-change skip. Docker: all containers run as host user via SEED_UID/SEED_GID, all bind mounts use :z for SELinux, rsync uses -rlt instead of -a with non-fatal wrapper. Deploy engine: OPS_BASE_URL resolves SEED_DEPLOY_URL/SEED_REPO_URL, pull-first fast recreate, legacy container cleanup, inline image pruning, self-update in headless mode. Tests: 115 tests, 257 assertions covering all pure functions, CLI parsing, cron building, and self-update. --- ops/deploy.sh | 22 +- ops/deploy.test.ts | 385 ++++++++++++++ ops/deploy.ts | 1109 ++++++++++++++++++++++++++++++++++++---- ops/dist/deploy.js | 189 ++++--- ops/docker-compose.yml | 26 +- 5 files changed, 1560 insertions(+), 171 deletions(-) diff --git a/ops/deploy.sh b/ops/deploy.sh index 0d720673a..bd3985e99 100755 --- a/ops/deploy.sh +++ b/ops/deploy.sh @@ -16,7 +16,7 @@ set -e SEED_DIR="${SEED_DIR:-/opt/seed}" SEED_BRANCH="${SEED_BRANCH:-main}" -GH_RAW="https://raw.githubusercontent.com/seed-hypermedia/seed/${SEED_BRANCH}/ops" +GH_RAW="${SEED_DEPLOY_URL:-https://raw.githubusercontent.com/seed-hypermedia/seed/${SEED_BRANCH}/ops}" MIN_GLIBC="2.25" command_exists() { @@ -114,5 +114,25 @@ ensure_dir "${SEED_DIR}" info "Downloading deployment script..." curl -fsSL "${GH_RAW}/dist/deploy.js" -o "${SEED_DIR}/deploy.js" +# Install the 'seed-deploy' CLI wrapper so users can run commands from anywhere. +# ~/.local/bin is user-writable and on PATH by default on modern Linux distros. +BUN_PATH="$(command -v bun)" +WRAPPER_DIR="${HOME}/.local/bin" +WRAPPER="${WRAPPER_DIR}/seed-deploy" + +mkdir -p "${WRAPPER_DIR}" +cat > "$WRAPPER" < { }); }); +// --------------------------------------------------------------------------- +// inferEnvironment +// --------------------------------------------------------------------------- + +function makeOldInstall( + overrides: Partial = {}, +): OldInstallInfo { + return { + workspace: "/home/user/.seed-site", + secret: null, + secretConsumed: false, + hostname: "https://example.com", + logLevel: "info", + imageTag: "latest", + testnet: false, + gateway: false, + trafficStats: false, + ...overrides, + }; +} + +describe("inferEnvironment", () => { + test("returns 'dev' when testnet is true", () => { + expect(inferEnvironment(makeOldInstall({ testnet: true }))).toBe("dev"); + }); + + test("returns 'dev' when testnet is true even with dev image tag", () => { + expect( + inferEnvironment(makeOldInstall({ testnet: true, imageTag: "dev" })), + ).toBe("dev"); + }); + + test("returns 'staging' when not testnet and image tag is 'dev'", () => { + expect( + inferEnvironment(makeOldInstall({ testnet: false, imageTag: "dev" })), + ).toBe("staging"); + }); + + test("returns 'prod' when not testnet and image tag is 'latest'", () => { + expect( + inferEnvironment(makeOldInstall({ testnet: false, imageTag: "latest" })), + ).toBe("prod"); + }); + + test("returns 'prod' when not testnet and image tag is a semver", () => { + expect( + inferEnvironment(makeOldInstall({ testnet: false, imageTag: "v1.2.3" })), + ).toBe("prod"); + }); + + test("returns 'prod' when not testnet and image tag is null", () => { + expect( + inferEnvironment(makeOldInstall({ testnet: false, imageTag: null })), + ).toBe("prod"); + }); +}); + // --------------------------------------------------------------------------- // buildComposeEnv // --------------------------------------------------------------------------- @@ -824,3 +889,323 @@ describe("full config scenarios", () => { expect(dirs.some((d) => d.includes("monitoring/prometheus"))).toBe(true); }); }); + +// --------------------------------------------------------------------------- +// buildCrontab +// --------------------------------------------------------------------------- + +describe("buildCrontab", () => { + const paths = makePaths("/opt/seed"); + + test("adds seed lines to an empty crontab", () => { + const result = buildCrontab("", paths); + expect(result).toContain("# seed-deploy"); + expect(result).toContain("# seed-cleanup"); + expect(result).toContain("/opt/seed/deploy.js"); + expect(result.endsWith("\n")).toBe(true); + }); + + test("preserves existing non-seed cron lines", () => { + const existing = "0 * * * * /usr/bin/some-other-job # my-job"; + const result = buildCrontab(existing, paths); + expect(result).toContain("some-other-job"); + expect(result).toContain("# seed-deploy"); + expect(result).toContain("# seed-cleanup"); + }); + + test("replaces existing seed-deploy line without duplicating", () => { + const existing = [ + "0 * * * * /usr/bin/some-other-job # my-job", + "0 3 * * * /usr/bin/bun /old/path/deploy.js >> /old/path/log 2>&1 # seed-deploy", + ].join("\n"); + const result = buildCrontab(existing, paths); + + // Should contain exactly one seed-deploy line (the new one) + const deployLines = result + .split("\n") + .filter((l) => l.includes("# seed-deploy")); + expect(deployLines).toHaveLength(1); + expect(deployLines[0]).toContain("/opt/seed/deploy.js"); + expect(deployLines[0]).not.toContain("/old/path"); + + // Other job preserved + expect(result).toContain("some-other-job"); + }); + + test("replaces existing seed-cleanup line without duplicating", () => { + const existing = "30 0 * * * docker image prune -f # seed-cleanup"; + const result = buildCrontab(existing, paths); + + const cleanupLines = result + .split("\n") + .filter((l) => l.includes("# seed-cleanup")); + expect(cleanupLines).toHaveLength(1); + // New version uses -a flag and multiple times + expect(cleanupLines[0]).toContain("0 0,4,8,12,16,20"); + }); + + test("replaces both seed lines at once (idempotent)", () => { + // First run + const first = buildCrontab("0 * * * * /usr/bin/other # my-job", paths); + // Second run with first output + const second = buildCrontab(first, paths); + + expect(first).toBe(second); + }); + + test("does not leave blank lines when replacing", () => { + const existing = [ + "0 * * * * /usr/bin/job-a # job-a", + "0 2 * * * /usr/bin/bun /opt/seed/deploy.js >> /opt/seed/deploy.log 2>&1 # seed-deploy", + "0 0,4,8,12,16,20 * * * docker image prune -a -f # seed-cleanup", + ].join("\n"); + const result = buildCrontab(existing, paths); + + // No double newlines (blank lines) + expect(result).not.toContain("\n\n"); + }); + + test("uses custom bun path in deploy cron line", () => { + const result = buildCrontab("", paths, "/home/user/.bun/bin/bun"); + const deployLine = result + .split("\n") + .find((l) => l.includes("# seed-deploy"))!; + expect(deployLine).toContain("/home/user/.bun/bin/bun"); + expect(deployLine).not.toContain("/usr/local/bin/bun"); + }); + + test("defaults bun path to /usr/local/bin/bun", () => { + const result = buildCrontab("", paths); + const deployLine = result + .split("\n") + .find((l) => l.includes("# seed-deploy"))!; + expect(deployLine).toContain("/usr/local/bin/bun"); + }); +}); + +// --------------------------------------------------------------------------- +// parseArgs +// --------------------------------------------------------------------------- + +describe("parseArgs", () => { + test("defaults to 'deploy' when no args given", () => { + const result = parseArgs(["node", "deploy.js"]); + expect(result.command).toBe("deploy"); + expect(result.args).toEqual([]); + }); + + test("parses known commands", () => { + const commands = [ + "deploy", + "stop", + "start", + "restart", + "status", + "config", + "logs", + "cron", + "backup", + "restore", + "uninstall", + ] as const; + for (const cmd of commands) { + const result = parseArgs(["node", "deploy.js", cmd]); + expect(result.command).toBe(cmd); + } + }); + + test("passes remaining args through", () => { + const result = parseArgs(["node", "deploy.js", "logs", "daemon"]); + expect(result.command).toBe("logs"); + expect(result.args).toEqual(["daemon"]); + }); + + test("parses --help flag", () => { + expect(parseArgs(["node", "deploy.js", "--help"]).command).toBe("help"); + expect(parseArgs(["node", "deploy.js", "-h"]).command).toBe("help"); + }); + + test("parses --version flag", () => { + expect(parseArgs(["node", "deploy.js", "--version"]).command).toBe( + "version", + ); + expect(parseArgs(["node", "deploy.js", "-v"]).command).toBe("version"); + }); + + test("passes backup path as arg", () => { + const result = parseArgs([ + "node", + "deploy.js", + "backup", + "/tmp/backup.tar.gz", + ]); + expect(result.command).toBe("backup"); + expect(result.args).toEqual(["/tmp/backup.tar.gz"]); + }); + + test("passes restore path as arg", () => { + const result = parseArgs([ + "node", + "deploy.js", + "restore", + "/tmp/backup.tar.gz", + ]); + expect(result.command).toBe("restore"); + expect(result.args).toEqual(["/tmp/backup.tar.gz"]); + }); + + test("passes cron subcommand as arg", () => { + const result = parseArgs(["node", "deploy.js", "cron", "remove"]); + expect(result.command).toBe("cron"); + expect(result.args).toEqual(["remove"]); + }); + + test("--reconfigure flag on deploy command", () => { + const result = parseArgs(["node", "deploy.js", "deploy", "--reconfigure"]); + expect(result.command).toBe("deploy"); + expect(result.reconfigure).toBe(true); + expect(result.args).toEqual([]); + }); + + test("--reconfigure without explicit deploy command", () => { + const result = parseArgs(["node", "deploy.js", "--reconfigure"]); + expect(result.command).toBe("deploy"); + expect(result.reconfigure).toBe(true); + expect(result.args).toEqual([]); + }); + + test("--reconfigure is not set on other commands", () => { + const result = parseArgs(["node", "deploy.js", "status"]); + expect(result.reconfigure).toBeFalsy(); + }); + + test("--reconfigure is not set on plain deploy", () => { + const result = parseArgs(["node", "deploy.js"]); + expect(result.reconfigure).toBeFalsy(); + }); +}); + +// --------------------------------------------------------------------------- +// extractSeedCronLines / removeSeedCronLines +// --------------------------------------------------------------------------- + +describe("extractSeedCronLines", () => { + test("extracts seed-deploy and seed-cleanup lines", () => { + const crontab = [ + "0 * * * * /usr/bin/other # my-job", + "0 2 * * * /usr/bin/bun /opt/seed/deploy.js >> /opt/seed/deploy.log 2>&1 # seed-deploy", + "0 0,4,8,12,16,20 * * * docker image prune -a -f # seed-cleanup", + ].join("\n"); + const lines = extractSeedCronLines(crontab); + expect(lines).toHaveLength(2); + expect(lines[0]).toContain("# seed-deploy"); + expect(lines[1]).toContain("# seed-cleanup"); + }); + + test("returns empty array when no seed lines present", () => { + const crontab = "0 * * * * /usr/bin/other # my-job"; + expect(extractSeedCronLines(crontab)).toHaveLength(0); + }); +}); + +describe("removeSeedCronLines", () => { + test("removes seed lines and preserves others", () => { + const crontab = [ + "0 * * * * /usr/bin/other # my-job", + "0 2 * * * /usr/bin/bun /opt/seed/deploy.js # seed-deploy", + "30 * * * * /usr/bin/another # another-job", + "0 0 * * * docker image prune -a -f # seed-cleanup", + ].join("\n"); + const result = removeSeedCronLines(crontab); + expect(result).toContain("my-job"); + expect(result).toContain("another-job"); + expect(result).not.toContain("seed-deploy"); + expect(result).not.toContain("seed-cleanup"); + }); + + test("returns empty crontab when only seed lines present", () => { + const crontab = [ + "0 2 * * * /usr/bin/bun /opt/seed/deploy.js # seed-deploy", + "0 0 * * * docker image prune -a -f # seed-cleanup", + ].join("\n"); + const result = removeSeedCronLines(crontab); + expect(result.trim()).toBe(""); + }); +}); + +// --------------------------------------------------------------------------- +// DEFAULT_SEED_DIR derivation +// --------------------------------------------------------------------------- + +describe("DEFAULT_SEED_DIR", () => { + test("derives from dirname of process.argv[1] when SEED_DIR is unset", () => { + // When SEED_DIR is not set in the test environment, the default should + // be derived from process.argv[1] (the test runner's script path). + // We can't fully test the env-var branch here since it would require + // modifying process.env before the module loads, but we verify the + // derivation logic is not hardcoded to "/opt/seed". + if (!process.env.SEED_DIR) { + const { dirname } = require("node:path"); + expect(DEFAULT_SEED_DIR).toBe(dirname(process.argv[1])); + } else { + expect(DEFAULT_SEED_DIR).toBe(process.env.SEED_DIR); + } + }); +}); + +// --------------------------------------------------------------------------- +// buildCrontab: pruning safety margin +// --------------------------------------------------------------------------- + +describe("buildCrontab pruning safety", () => { + const paths = makePaths("/opt/seed"); + + test("cleanup line includes --filter until=1h for safety margin", () => { + const result = buildCrontab("", paths); + const cleanupLine = result + .split("\n") + .find((l) => l.includes("# seed-cleanup"))!; + expect(cleanupLine).toContain('--filter "until=1h"'); + expect(cleanupLine).toContain("docker image prune -a -f"); + }); +}); + +// --------------------------------------------------------------------------- +// selfUpdate +// --------------------------------------------------------------------------- + +describe("selfUpdate", () => { + let tmpDir: string; + let paths: DeployPaths; + + beforeEach(async () => { + tmpDir = await mkdtemp(join(tmpdir(), "seed-selfupdate-")); + paths = makePaths(tmpDir); + await mkdir(tmpDir, { recursive: true }); + }); + + afterEach(async () => { + await rm(tmpDir, { recursive: true, force: true }); + }); + + test("is exported and callable", () => { + expect(typeof selfUpdate).toBe("function"); + }); + + test("handles fetch failure gracefully (no throw)", async () => { + // selfUpdate uses process.argv[1] and fetches from DEFAULT_REPO_URL. + // In a test environment, the fetch will fail (no internet or wrong URL). + // It should not throw β€” just log and return. + const origUrl = process.env.SEED_REPO_URL; + process.env.SEED_REPO_URL = "http://localhost:1/nonexistent"; + try { + await selfUpdate(paths); // should not throw + } finally { + if (origUrl !== undefined) { + process.env.SEED_REPO_URL = origUrl; + } else { + delete process.env.SEED_REPO_URL; + } + } + }); +}); diff --git a/ops/deploy.ts b/ops/deploy.ts index d46f0201d..336d3165b 100644 --- a/ops/deploy.ts +++ b/ops/deploy.ts @@ -11,24 +11,35 @@ * of the new deployment system. When it exists, the script runs headless. * When it doesn't, it runs the interactive wizard. * - * The seed directory defaults to /opt/seed but can be overridden via - * the SEED_DIR environment variable. + * The seed directory is derived from the script's own location + * (dirname of process.argv[1]) so that cron jobs and the seed-deploy + * wrapper always resolve to the correct path. This can be overridden + * via the SEED_DIR environment variable. */ import * as p from "@clack/prompts"; -import { readFile, writeFile, mkdir, access } from "node:fs/promises"; +import { readFile, writeFile, mkdir, access, stat } from "node:fs/promises"; import { execSync, exec as execCb } from "node:child_process"; import { createHash, randomBytes } from "node:crypto"; import { homedir } from "node:os"; -import { join } from "node:path"; +import { join, basename, dirname } from "node:path"; export const VERSION = "0.1.0"; -export const DEFAULT_SEED_DIR = process.env.SEED_DIR || "/opt/seed"; +export const DEFAULT_SEED_DIR = + process.env.SEED_DIR || dirname(process.argv[1]); export const DEFAULT_REPO_URL = "https://raw.githubusercontent.com/seed-hypermedia/seed/main"; -export const DEFAULT_COMPOSE_URL = `${ - process.env.SEED_REPO_URL || DEFAULT_REPO_URL -}/ops/docker-compose.yml`; + +// SEED_DEPLOY_URL points at the ops/ directory (matches deploy.sh convention). +// SEED_REPO_URL points at the repo root (legacy, kept for compatibility). +// Both resolve to the same ops/ base for fetching compose + deploy.js. +export const OPS_BASE_URL = + process.env.SEED_DEPLOY_URL || + (process.env.SEED_REPO_URL + ? `${process.env.SEED_REPO_URL}/ops` + : `${DEFAULT_REPO_URL}/ops`); + +export const DEFAULT_COMPOSE_URL = `${OPS_BASE_URL}/docker-compose.yml`; export const NOTIFY_SERVICE_HOST = "https://notify.seed.hyper.media"; export const LIGHTNING_URL_MAINNET = "https://ln.seed.hyper.media"; export const LIGHTNING_URL_TESTNET = "https://ln.testnet.seed.hyper.media"; @@ -180,6 +191,10 @@ export function log(msg: string): void { } } +const RESUME_HINT = "\nRun 'seed-deploy' to resume installation at any time.\n"; +const MANAGE_HINT = + "Manage your node anytime with the 'seed-deploy' command. Run 'seed-deploy --help' for options."; + // --------------------------------------------------------------------------- // Old installation detection // --------------------------------------------------------------------------- @@ -187,6 +202,8 @@ export function log(msg: string): void { export interface OldInstallInfo { workspace: string; secret: string | null; + /** True when the old config.json exists but the secret field was already consumed (node registered). */ + secretConsumed: boolean; hostname: string | null; logLevel: string | null; imageTag: string | null; @@ -195,6 +212,15 @@ export interface OldInstallInfo { trafficStats: boolean; } +/** Infer the environment preset from the old installation's state. */ +export function inferEnvironment( + old: OldInstallInfo, +): "prod" | "staging" | "dev" { + if (old.testnet) return "dev"; + if (old.imageTag === "dev") return "staging"; + return "prod"; +} + export function parseDaemonEnv(envJson: string): { logLevel: string | null; testnet: boolean; @@ -270,6 +296,7 @@ export async function detectOldInstall( } let secret: string | null = null; + let secretConsumed = false; const secretPaths = [ join(workspace, "web", "config.json"), "/shm/gateway/web/config.json", @@ -283,6 +310,10 @@ export async function detectOldInstall( secret = parsed.availableRegistrationSecret; break; } + // Config exists but secret is missing β€” it was consumed during registration. + if (parsed.registeredAccountUid || parsed.sourcePeerId) { + secretConsumed = true; + } } catch { // try next } @@ -324,6 +355,7 @@ export async function detectOldInstall( return { workspace, secret, + secretConsumed, hostname, logLevel, imageTag, @@ -361,28 +393,17 @@ async function runMigrationWizard( domain: () => p.text({ message: "Public hostname (including https://)", - placeholder: "https://node1.seed.run", - initialValue: old.hostname ?? "", + placeholder: old.hostname || "https://node1.seed.run", validate: (v) => { if (!v) return "Required"; if (!v.startsWith("https://") && !v.startsWith("http://")) return "Must start with https:// or http://"; }, }), - email: () => - p.text({ - message: - "Contact email β€” lets us notify you about security updates and node issues. Not shared publicly.", - placeholder: "you@example.com", - validate: (v) => { - if (!v) return "Required"; - if (!v.includes("@")) return "Must be a valid email"; - }, - }), environment: () => p.select({ message: "Environment", - initialValue: old.testnet ? "dev" : "prod", + initialValue: inferEnvironment(old), options: [ { value: "prod", @@ -431,10 +452,20 @@ async function runMigrationWizard( "Enable web analytics? Adds a Plausible.io dashboard to track your site's traffic.", initialValue: old.trafficStats, }), + email: () => + p.text({ + message: + "Contact email (optional) β€” lets us notify you about security updates. Not shared publicly.", + placeholder: "you@example.com", + validate: (v) => { + if (v && !v.includes("@")) return "Must be a valid email"; + }, + }), }, { onCancel: () => { - p.cancel("Migration cancelled"); + p.cancel("Migration cancelled."); + console.log(RESUME_HINT); process.exit(0); }, }, @@ -443,6 +474,10 @@ async function runMigrationWizard( const secret = old.secret ?? generateSecret(); if (old.secret) { p.log.success(`Registration secret imported from existing installation.`); + } else if (old.secretConsumed) { + p.log.info( + `Node is already registered (secret was consumed). Generated a new secret for future registrations.`, + ); } else { p.log.warn(`No existing registration secret found. Generated a new one.`); } @@ -452,7 +487,7 @@ async function runMigrationWizard( const config: SeedConfig = { domain: answers.domain as string, - email: answers.email as string, + email: (answers.email as string) || "", compose_url: DEFAULT_COMPOSE_URL, compose_sha: "", compose_envs: { @@ -477,7 +512,8 @@ async function runMigrationWizard( }); if (p.isCancel(confirmed) || !confirmed) { - p.cancel("Migration cancelled"); + p.cancel("Migration cancelled."); + console.log(RESUME_HINT); process.exit(0); } @@ -513,49 +549,58 @@ async function runMigrationWizard( // Fresh Install Wizard // --------------------------------------------------------------------------- -async function runFreshWizard(paths: DeployPaths): Promise { - p.intro(`Seed Node Setup v${VERSION}`); - - p.note( - [ - "Welcome! This wizard will configure your new Seed node.", - "", - "Seed is a peer-to-peer hypermedia publishing system. This script sets up", - "the Docker containers, reverse proxy, and networking so your node is", - "reachable on the public internet.", - "", - `Configuration will be saved to ${paths.configPath}.`, - "Subsequent runs of this script will deploy automatically (headless mode).", - ].join("\n"), - "First-time setup", +async function runFreshWizard( + paths: DeployPaths, + existing?: SeedConfig, +): Promise { + const isReconfig = !!existing; + p.intro( + isReconfig + ? `Seed Node Reconfiguration v${VERSION}` + : `Seed Node Setup v${VERSION}`, ); + if (!isReconfig) { + p.note( + [ + "Welcome! This wizard will configure your new Seed node.", + "", + "Seed is a peer-to-peer hypermedia publishing system. This script sets up", + "the Docker containers, reverse proxy, and networking so your node is", + "reachable on the public internet.", + "", + `Configuration will be saved to ${paths.configPath}.`, + "Subsequent runs of this script will deploy automatically (headless mode).", + ].join("\n"), + "First-time setup", + ); + } else { + p.note( + [ + "Editing your current configuration. Press Tab to keep existing values, or type to change them.", + "", + `Configuration: ${paths.configPath}`, + ].join("\n"), + "Reconfiguration", + ); + } + const answers = await p.group( { domain: () => p.text({ message: "Public hostname (including https://)", - placeholder: "https://node1.seed.run", + placeholder: existing?.domain || "https://node1.seed.run", validate: (v) => { if (!v) return "Required"; if (!v.startsWith("https://") && !v.startsWith("http://")) return "Must start with https:// or http://"; }, }), - email: () => - p.text({ - message: - "Contact email β€” lets us notify you about security updates and node issues. Not shared publicly.", - placeholder: "you@example.com", - validate: (v) => { - if (!v) return "Required"; - if (!v.includes("@")) return "Must be a valid email"; - }, - }), environment: () => p.select({ message: "Environment", - initialValue: "prod", + initialValue: existing?.environment ?? "prod", options: [ { value: "prod", @@ -577,7 +622,7 @@ async function runFreshWizard(paths: DeployPaths): Promise { log_level: () => p.select({ message: "Log level for Seed services", - initialValue: "info", + initialValue: existing?.compose_envs?.LOG_LEVEL ?? "info", options: [ { value: "debug", @@ -596,33 +641,45 @@ async function runFreshWizard(paths: DeployPaths): Promise { gateway: () => p.confirm({ message: "Run as a public gateway? (serves all known public content)", - initialValue: false, + initialValue: existing?.gateway ?? false, }), analytics: () => p.confirm({ message: "Enable web analytics? Adds a Plausible.io dashboard to track your site's traffic.", - initialValue: false, + initialValue: existing?.analytics ?? false, + }), + email: () => + p.text({ + message: + "Contact email (optional) β€” lets us notify you about security updates. Not shared publicly.", + placeholder: existing?.email || "you@example.com", + validate: (v) => { + if (v && !v.includes("@")) return "Must be a valid email"; + }, }), }, { onCancel: () => { - p.cancel("Setup cancelled"); + p.cancel( + isReconfig ? "Reconfiguration cancelled." : "Setup cancelled.", + ); + console.log(RESUME_HINT); process.exit(0); }, }, ); - const secret = generateSecret(); + const secret = existing?.link_secret ?? generateSecret(); const env = answers.environment as SeedConfig["environment"]; const presets = environmentPresets(env); const config: SeedConfig = { domain: answers.domain as string, - email: answers.email as string, + email: (answers.email as string) || "", compose_url: DEFAULT_COMPOSE_URL, - compose_sha: "", + compose_sha: existing?.compose_sha ?? "", compose_envs: { LOG_LEVEL: answers.log_level as SeedConfig["compose_envs"]["LOG_LEVEL"], }, @@ -632,21 +689,46 @@ async function runFreshWizard(paths: DeployPaths): Promise { link_secret: secret, analytics: answers.analytics as boolean, gateway: answers.gateway as boolean, - last_script_run: "", + last_script_run: existing?.last_script_run ?? "", }; - const summary = Object.entries(config) - .filter(([k]) => k !== "compose_sha" && k !== "last_script_run") - .map(([k, v]) => ` ${k}: ${typeof v === "object" ? JSON.stringify(v) : v}`) + const userFields: [string, string][] = [ + ["domain", config.domain], + ["email", config.email], + ["environment", config.environment], + ["log_level", config.compose_envs.LOG_LEVEL], + ["gateway", String(config.gateway)], + ["analytics", String(config.analytics)], + ]; + const oldFields: Record | undefined = existing + ? { + domain: existing.domain, + email: existing.email, + environment: existing.environment, + log_level: existing.compose_envs?.LOG_LEVEL ?? "info", + gateway: String(existing.gateway), + analytics: String(existing.analytics), + } + : undefined; + const summary = userFields + .map(([k, v]) => { + if (oldFields && String(v) !== String(oldFields[k] ?? "")) { + return ` \u270E ${k}: ${v}`; + } + return ` ${k}: ${v}`; + }) .join("\n"); p.note(summary, "Configuration summary"); const confirmed = await p.confirm({ - message: "Write config and proceed with deployment?", + message: isReconfig + ? "Save changes and redeploy?" + : "Write config and proceed with deployment?", }); if (p.isCancel(confirmed) || !confirmed) { - p.cancel("Setup cancelled"); + p.cancel(isReconfig ? "Reconfiguration cancelled." : "Setup cancelled."); + console.log(RESUME_HINT); process.exit(0); } @@ -803,6 +885,43 @@ async function rollback( log("Rollback complete. Check container status with: docker ps"); } +// --------------------------------------------------------------------------- +// Self-update β€” fetches the latest deploy.js from the repo during headless +// runs so that cron-triggered deployments always use the newest script. +// The update takes effect on the *next* run; the current process continues +// with the code already loaded in memory. +// --------------------------------------------------------------------------- + +export async function selfUpdate(paths: DeployPaths): Promise { + const scriptPath = process.argv[1]; + const url = `${OPS_BASE_URL}/dist/deploy.js`; + + try { + const response = await fetch(url); + if (!response.ok) { + log(`Self-update: failed to fetch ${url}: ${response.status}`); + return; + } + const remote = await response.text(); + + let local = ""; + try { + local = await readFile(scriptPath, "utf-8"); + } catch { + // First run or path mismatch β€” treat as stale + } + + if (sha256(remote) !== sha256(local)) { + await writeFile(scriptPath, remote, "utf-8"); + log(`Self-update: deploy.js updated (takes effect on next run).`); + } else { + log("Self-update: deploy.js is up to date."); + } + } catch (err) { + log(`Self-update: skipped (${err})`); + } +} + export async function deploy( config: SeedConfig, paths: DeployPaths, @@ -823,9 +942,12 @@ export async function deploy( spinner?.start("Fetching docker-compose.yml..."); step("Fetching docker-compose.yml..."); - const repoOverride = process.env.SEED_REPO_URL; - const composeUrl = repoOverride - ? `${repoOverride}/ops/docker-compose.yml` + // Use env-based URL override when present (testing / branch builds), + // otherwise fall back to the compose_url stored in config.json. + const hasEnvOverride = + process.env.SEED_DEPLOY_URL || process.env.SEED_REPO_URL; + const composeUrl = hasEnvOverride + ? `${OPS_BASE_URL}/docker-compose.yml` : config.compose_url; const composeResponse = await fetch(composeUrl); if (!composeResponse.ok) { @@ -845,6 +967,11 @@ export async function deploy( log( "No changes detected β€” compose SHA matches and containers are healthy. Skipping.", ); + if (isInteractive) { + console.log( + "\n To change your node's configuration, run 'seed-deploy deploy --reconfigure'.\n", + ); + } config.last_script_run = new Date().toISOString(); await writeConfig(config, paths); return; @@ -889,21 +1016,40 @@ export async function deploy( step("Created initial web/config.json with registration secret."); } - step("Stopping any existing containers..."); - shell.runSafe( - "docker stop seed-site seed-daemon seed-proxy grafana prometheus 2>/dev/null", - ); - shell.runSafe( - "docker rm seed-site seed-daemon seed-proxy grafana prometheus 2>/dev/null", - ); - const previousImages = await getContainerImages(shell); - - step("Running docker compose up..."); const env = buildComposeEnv(config, paths); + // On first compose-managed deploy, remove legacy containers that were + // created by the old website_deployment.sh via `docker run`. They aren't + // compose-managed, so `docker compose up` can't recreate them and will + // fail with a "name already in use" conflict. + if (!config.compose_sha) { + step("Removing legacy containers..."); + shell.runSafe( + "docker stop seed-site seed-daemon seed-web seed-proxy autoupdater grafana prometheus 2>/dev/null", + ); + shell.runSafe( + "docker rm seed-site seed-daemon seed-web seed-proxy autoupdater grafana prometheus 2>/dev/null", + ); + } + + // Pull new images while existing containers keep serving traffic. + // This eliminates image download time from the downtime window. + step("Pulling latest images..."); + try { + await shell.exec( + `${env} docker compose -f ${paths.composePath} pull --quiet`, + ); + } catch (err: unknown) { + log(`Image pull failed: ${err}`); + // Non-fatal β€” compose up will attempt to pull as fallback + } + + // Recreate containers from pre-pulled images. Only containers whose + // image or configuration changed will be recreated; the rest stay up. + step("Recreating containers..."); try { - const composeCmd = `${env} docker compose -f ${paths.composePath} up -d --pull always --quiet-pull`; + const composeCmd = `${env} docker compose -f ${paths.composePath} up -d --quiet-pull`; const result = await shell.exec(composeCmd); if (result.stderr) { log(`compose stderr: ${result.stderr}`); @@ -917,13 +1063,12 @@ export async function deploy( throw new Error(`Deployment failed: ${err}`); } - step("Running post-deploy health checks..."); let healthy = false; for (let attempt = 0; attempt < 10; attempt++) { + step(`Health check ${attempt + 1}/10...`); await new Promise((r) => setTimeout(r, 3000)); healthy = await checkContainersHealthy(shell); if (healthy) break; - step(`Health check attempt ${attempt + 1}/10...`); } if (!healthy) { @@ -941,9 +1086,17 @@ export async function deploy( config.last_script_run = new Date().toISOString(); await writeConfig(config, paths); + // Prune old images immediately so disk doesn't fill up between cron runs. + step("Cleaning up unused images..."); + shell.runSafe('docker image prune -a -f --filter "until=1m" 2>/dev/null'); + spinner?.stop("Deployment complete!"); log("Deployment complete."); + if (isInteractive) { + p.log.message(MANAGE_HINT); + } + if (isInteractive && isFirstDeploy) { // TODO: POST config.email, config.domain, and config.analytics to the // Seed vault so the team knows about new deployments and can activate @@ -967,44 +1120,176 @@ export async function deploy( // Cron Setup // --------------------------------------------------------------------------- +/** + * Build an updated crontab string by replacing any existing seed-managed lines + * (identified by `# seed-deploy` / `# seed-cleanup` comment markers) with the + * current versions. Non-seed lines are preserved untouched. + */ +export function buildCrontab( + existing: string, + paths: DeployPaths, + bunPath: string = "/usr/local/bin/bun", +): string { + const deployLine = `0 2 * * * ${bunPath} ${join(paths.seedDir, "deploy.js")} >> ${paths.deployLog} 2>&1 # seed-deploy`; + const cleanupLine = `0 0,4,8,12,16,20 * * * docker image prune -a -f --filter "until=1h" # seed-cleanup`; + + const filtered = existing + .split("\n") + .filter( + (line) => + !line.includes("# seed-deploy") && !line.includes("# seed-cleanup"), + ) + .join("\n") + .trim(); + + return [filtered, deployLine, cleanupLine].filter(Boolean).join("\n") + "\n"; +} + export async function setupCron( paths: DeployPaths, shell: ShellRunner, ): Promise { - const cronLine = `0 2 * * * /usr/bin/bun ${join(paths.seedDir, "deploy.js")} >> ${paths.deployLog} 2>&1 # seed-deploy`; - + const bunPath = shell.runSafe("which bun") ?? "/usr/local/bin/bun"; const existing = shell.runSafe("crontab -l 2>/dev/null") ?? ""; - if (existing.includes("seed-deploy")) { - log("Cron job already installed. Skipping."); - return; - } + const newCrontab = buildCrontab(existing, paths, bunPath); - const cleanupCron = `0 0,4,8,12,16,20 * * * docker image prune -a -f # seed-cleanup`; - const newCrontab = - [existing, cronLine, cleanupCron].filter(Boolean).join("\n") + "\n"; try { - execSync(`echo '${newCrontab}' | crontab -`, { encoding: "utf-8" }); - log( - "Installed nightly deployment cron job (02:00) and image cleanup cron.", - ); + shell.run(`echo '${newCrontab}' | crontab -`); + if ( + existing.includes("# seed-deploy") || + existing.includes("# seed-cleanup") + ) { + log("Updated existing seed cron jobs."); + } else { + log( + "Installed nightly deployment cron job (02:00) and image cleanup cron.", + ); + } } catch (err) { log(`Warning: Failed to install cron job: ${err}`); } } // --------------------------------------------------------------------------- -// Main +// CLI: Argument Parsing // --------------------------------------------------------------------------- -async function main(): Promise { - const paths = makePaths(); - const shell = makeShellRunner(); +const COMMANDS = [ + "deploy", + "stop", + "start", + "restart", + "status", + "config", + "logs", + "cron", + "backup", + "restore", + "uninstall", +] as const; + +export type CliCommand = (typeof COMMANDS)[number]; + +export interface CliArgs { + command: CliCommand | "help" | "version"; + args: string[]; + reconfigure?: boolean; +} + +export function parseArgs(argv: string[] = process.argv): CliArgs { + const raw = argv.slice(2); + const first = raw[0] ?? "deploy"; + + if (first === "--help" || first === "-h") + return { command: "help", args: [] }; + if (first === "--version" || first === "-v") + return { command: "version", args: [] }; + if (first === "--reconfigure") + return { command: "deploy", args: [], reconfigure: true }; + + if ((COMMANDS as readonly string[]).includes(first)) { + const rest = raw.slice(1); + const reconfigure = first === "deploy" && rest.includes("--reconfigure"); + const args = reconfigure ? rest.filter((a) => a !== "--reconfigure") : rest; + return { command: first as CliCommand, args, reconfigure }; + } + + console.error(`Unknown command: ${first}\n`); + printHelp(); + process.exit(1); +} + +export function printHelp(): void { + const text = ` +Seed Node Deployment v${VERSION} + +Usage: seed-deploy [command] [options] + +Commands: + deploy Deploy or update the Seed node (default) + stop Stop and remove all Seed containers + start Start containers without re-deploying + restart Restart all Seed containers + status Show node health, versions, and connectivity + config Print current configuration (secrets redacted) + logs Tail container logs [daemon|web|proxy] + cron Install or remove automatic update cron jobs + backup Create a portable backup of all node data + restore Restore node data from a backup file + uninstall Remove all Seed containers, data, and configuration + +Options: + --reconfigure Re-run the setup wizard to change configuration + -h, --help Show this help message + -v, --version Show script version + +Examples: + seed-deploy Deploy or update + seed-deploy deploy --reconfigure Change node configuration + seed-deploy stop Teardown containers + seed-deploy status Check node health + seed-deploy logs daemon Tail seed-daemon logs + seed-deploy cron Install automatic update cron + seed-deploy cron remove Remove cron jobs + seed-deploy backup Create backup + seed-deploy backup /tmp/backup.tar.gz Create backup at custom path + seed-deploy restore backup.tar.gz Restore from backup file + +The 'seed-deploy' command is installed at ~/.local/bin/seed-deploy +during initial setup. The deployment script lives at ${DEFAULT_SEED_DIR}/deploy.js. +`.trimStart(); + console.log(text); +} + +// --------------------------------------------------------------------------- +// CLI: Commands +// --------------------------------------------------------------------------- +async function cmdDeploy( + paths: DeployPaths, + shell: ShellRunner, + reconfigure = false, +): Promise { await ensureSeedDir(paths, shell); - const hasConfig = await configExists(paths); + // In headless mode (cron), self-update the script before deploying. + // The update takes effect on the next run β€” the current process keeps + // executing with the code already loaded in memory. + if (!process.stdout.isTTY) { + await selfUpdate(paths); + } + + if (await configExists(paths)) { + if (reconfigure && process.stdout.isTTY) { + const existing = await readConfig(paths); + const config = await runFreshWizard(paths, existing); + await deploy(config, paths, shell); + p.outro( + `Reconfiguration complete! Your Seed node is running.\n${MANAGE_HINT}`, + ); + return; + } - if (hasConfig) { log( `Seed deploy v${VERSION} β€” config found at ${paths.configPath}, running headless.`, ); @@ -1035,7 +1320,631 @@ async function main(): Promise { await deploy(config, paths, shell); - p.outro("Setup complete! Your Seed node is running."); + p.outro(`Setup complete! Your Seed node is running.\n${MANAGE_HINT}`); +} + +async function cmdStop(paths: DeployPaths, shell: ShellRunner): Promise { + console.log("Stopping and removing Seed containers..."); + shell.runSafe(`docker compose -f "${paths.composePath}" down`); + console.log("All Seed containers stopped and removed."); +} + +async function cmdStart(paths: DeployPaths, shell: ShellRunner): Promise { + if (!(await configExists(paths))) { + console.error( + `No config found at ${paths.configPath}. Run 'seed-deploy' first to set up.`, + ); + process.exit(1); + } + + const config = await readConfig(paths); + const envContent = buildComposeEnv(config, paths); + + console.log("Starting Seed containers..."); + shell.run( + `${envContent} docker compose -f "${paths.composePath}" up -d --quiet-pull`, + ); + console.log("Seed containers started."); +} + +async function cmdRestart( + paths: DeployPaths, + shell: ShellRunner, +): Promise { + await cmdStop(paths, shell); + await cmdStart(paths, shell); +} + +async function cmdStatus( + paths: DeployPaths, + shell: ShellRunner, +): Promise { + console.log(`\nSeed Node Status v${VERSION}`); + console.log("━".repeat(40)); + + // Configuration + let config: SeedConfig | null = null; + if (await configExists(paths)) { + config = await readConfig(paths); + console.log(`\nConfiguration:`); + console.log(` Domain: ${config.domain}`); + console.log( + ` Environment: ${{ prod: "Production", staging: "Staging", dev: "Development" }[config.environment]}`, + ); + console.log(` Channel: ${config.release_channel}`); + console.log(` Gateway: ${config.gateway ? "Yes" : "No"}`); + console.log(` Analytics: ${config.analytics ? "Yes" : "No"}`); + console.log(` Config: ${paths.configPath}`); + } else { + console.log( + `\nNo config found at ${paths.configPath}. Node is not set up.`, + ); + } + + // Containers + console.log(`\nContainers:`); + const containers = ["seed-daemon", "seed-web", "seed-proxy"]; + let hasUnhealthy = false; + for (const name of containers) { + const status = shell.runSafe( + `docker inspect ${name} --format '{{.State.Status}}' 2>/dev/null`, + ); + const image = shell.runSafe( + `docker inspect ${name} --format '{{.Config.Image}}' 2>/dev/null`, + ); + const started = shell.runSafe( + `docker inspect ${name} --format '{{.State.StartedAt}}' 2>/dev/null`, + ); + if (status) { + const symbol = status === "running" ? "\u2714" : "\u26A0"; + console.log( + ` ${symbol} ${name.padEnd(14)} ${(status ?? "").padEnd(10)} ${image ?? ""}${started ? ` (since ${started})` : ""}`, + ); + if (status !== "running") { + hasUnhealthy = true; + const lastLog = shell.runSafe(`docker logs --tail 1 ${name} 2>&1`); + if (lastLog) { + console.log(` β”” ${lastLog.slice(0, 120)}`); + } + } + } else { + console.log(` \u2718 ${name.padEnd(14)} not found`); + } + } + + if (hasUnhealthy) { + console.log(`\n Tip: Check logs with 'seed-deploy logs daemon|web|proxy'`); + } + + // Monitoring export check (only shown when metrics profile is active) + const prometheusRunning = shell.runSafe( + `docker inspect prometheus --format '{{.State.Status}}' 2>/dev/null`, + ); + const grafanaRunning = shell.runSafe( + `docker inspect grafana --format '{{.State.Status}}' 2>/dev/null`, + ); + if (prometheusRunning || grafanaRunning) { + const prometheusConfig = join( + paths.seedDir, + "monitoring", + "prometheus", + "prometheus.yaml", + ); + const grafanaProvDir = join( + paths.seedDir, + "monitoring", + "grafana", + "provisioning", + ); + let monitoringOk = true; + + console.log(`\nMonitoring:`); + try { + await access(prometheusConfig); + console.log(` \u2714 Prometheus config exported`); + } catch { + monitoringOk = false; + console.log(` \u26A0 Prometheus config not exported`); + } + try { + await access(grafanaProvDir); + console.log(` \u2714 Grafana provisioning exported`); + } catch { + monitoringOk = false; + console.log(` \u26A0 Grafana provisioning not exported`); + } + if (!monitoringOk) { + console.log( + `\n Tip: This may indicate a permissions issue with the monitoring/ directory.`, + ); + console.log( + ` Run 'seed-deploy deploy' to attempt an automatic fix.`, + ); + } + } + + // Health checks (only if config exists and domain is set) + if (config) { + console.log(`\nHealth Checks:`); + const dns = extractDns(config.domain); + + // HTTPS check + const httpCode = shell.runSafe( + `curl -sSf -o /dev/null -w '%{http_code}' --max-time 10 "${config.domain}" 2>/dev/null`, + ); + if (httpCode && httpCode.startsWith("2")) { + console.log(` \u2714 HTTPS ${httpCode} OK`); + } else if (httpCode) { + console.log(` \u26A0 HTTPS ${httpCode}`); + } else { + console.log(` \u26A0 HTTPS unreachable`); + } + + // DNS check + const publicIp = shell.runSafe( + "curl -s --max-time 5 ifconfig.me 2>/dev/null", + ); + const dnsResult = shell.runSafe( + `dig +short ${dns} A 2>/dev/null | head -1`, + ); + if (publicIp && dnsResult) { + if (dnsResult.trim() === publicIp.trim()) { + console.log( + ` \u2714 DNS ${dns} -> ${dnsResult} (matches public IP)`, + ); + } else { + console.log( + ` \u26A0 DNS ${dns} -> ${dnsResult} (public IP is ${publicIp})`, + ); + } + } else if (!dnsResult) { + console.log(` \u26A0 DNS ${dns} does not resolve`); + } else { + console.log(` ? DNS could not determine public IP`); + } + + // TLS certificate check + const certExpiry = shell.runSafe( + `echo | openssl s_client -servername "${dns}" -connect "${dns}:443" 2>/dev/null | openssl x509 -noout -enddate 2>/dev/null`, + ); + if (certExpiry) { + const expiryDate = certExpiry.replace("notAfter=", ""); + const expiry = new Date(expiryDate); + const daysLeft = Math.floor((expiry.getTime() - Date.now()) / 86_400_000); + const symbol = daysLeft > 14 ? "\u2714" : "\u26A0"; + console.log( + ` ${symbol} Certificate valid, expires ${expiry.toISOString().slice(0, 10)} (${daysLeft}d)`, + ); + } else { + console.log(` \u26A0 Certificate could not check`); + } + } + + // Disk usage + const du = shell.runSafe(`du -sh "${paths.seedDir}" 2>/dev/null`); + if (du) { + console.log(`\nDisk:`); + console.log(` ${paths.seedDir} ${du.split("\t")[0]}`); + } + + // Cron + const crontab = shell.runSafe("crontab -l 2>/dev/null") ?? ""; + const deployCron = crontab + .split("\n") + .find((l) => l.includes("# seed-deploy")); + const cleanupCron = crontab + .split("\n") + .find((l) => l.includes("# seed-cleanup")); + console.log(`\nCron:`); + console.log( + ` Auto-update: ${deployCron ? deployCron.split(" ").slice(0, 5).join(" ") : "not installed"}`, + ); + console.log( + ` Cleanup: ${cleanupCron ? cleanupCron.split(" ").slice(0, 5).join(" ") : "not installed"}`, + ); + if (!deployCron || !cleanupCron) { + console.log(`\n Tip: Run 'seed-deploy cron' to set up automatic updates.`); + } + + console.log(""); +} + +async function cmdConfig(paths: DeployPaths): Promise { + if (!(await configExists(paths))) { + console.error( + `No config found at ${paths.configPath}. Run 'seed-deploy' first.`, + ); + process.exit(1); + } + + const config = await readConfig(paths); + const redacted = { ...config, link_secret: "****" }; + console.log(JSON.stringify(redacted, null, 2)); +} + +async function cmdLogs(paths: DeployPaths, args: string[]): Promise { + const service = args[0]; + const serviceName = service ? `seed-${service}` : ""; + try { + execSync( + `docker compose -f "${paths.composePath}" logs -f --tail 100 ${serviceName}`, + { stdio: "inherit" }, + ); + } catch { + // User pressed Ctrl+C β€” normal exit + } +} + +async function cmdCron( + paths: DeployPaths, + shell: ShellRunner, + args: string[], +): Promise { + const subcommand = args[0] ?? "install"; + + if (subcommand === "remove") { + const existing = shell.runSafe("crontab -l 2>/dev/null") ?? ""; + if ( + !existing.includes("# seed-deploy") && + !existing.includes("# seed-cleanup") + ) { + console.log("No seed cron jobs found. Nothing to remove."); + return; + } + const cleaned = removeSeedCronLines(existing); + try { + shell.run(`echo '${cleaned}' | crontab -`); + console.log("Seed cron jobs removed."); + } catch (err) { + console.error(`Failed to remove cron jobs: ${err}`); + process.exit(1); + } + return; + } + + if (subcommand === "install") { + await setupCron(paths, shell); + const crontab = shell.runSafe("crontab -l 2>/dev/null") ?? ""; + const deployCron = crontab + .split("\n") + .find((l) => l.includes("# seed-deploy")); + const cleanupCron = crontab + .split("\n") + .find((l) => l.includes("# seed-cleanup")); + console.log("Cron jobs installed:"); + console.log(` Auto-update: ${deployCron ?? "(missing)"}`); + console.log(` Cleanup: ${cleanupCron ?? "(missing)"}`); + return; + } + + console.error(`Unknown cron subcommand: ${subcommand}`); + console.error("Usage: seed-deploy cron [install|remove]"); + process.exit(1); +} + +/** Extract seed-managed cron lines from a full crontab string. */ +export function extractSeedCronLines(crontab: string): string[] { + return crontab + .split("\n") + .filter( + (line) => + line.includes("# seed-deploy") || line.includes("# seed-cleanup"), + ); +} + +/** Remove all seed-managed cron lines from the active crontab. */ +export function removeSeedCronLines(existing: string): string { + return ( + existing + .split("\n") + .filter( + (line) => + !line.includes("# seed-deploy") && !line.includes("# seed-cleanup"), + ) + .join("\n") + .trim() + "\n" + ); +} + +/** Metadata stored inside backup archives for provenance tracking. */ +export interface BackupMeta { + version: string; + timestamp: string; + hostname: string; + seedDir: string; + cron: string[]; +} + +async function cmdBackup( + paths: DeployPaths, + shell: ShellRunner, + args: string[], +): Promise { + if (!(await configExists(paths))) { + console.error( + `No config found at ${paths.configPath}. Nothing to back up.`, + ); + process.exit(1); + } + + const config = await readConfig(paths); + const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); + const defaultPath = join( + paths.seedDir, + "backups", + `seed-backup-${timestamp}.tar.gz`, + ); + const backupFile = args[0] || defaultPath; + const backupDir = dirname(backupFile); + + await mkdir(backupDir, { recursive: true }); + + // Write backup metadata + const crontab = shell.runSafe("crontab -l 2>/dev/null") ?? ""; + const meta: BackupMeta = { + version: VERSION, + timestamp: new Date().toISOString(), + hostname: config.domain, + seedDir: paths.seedDir, + cron: extractSeedCronLines(crontab), + }; + await writeFile( + join(paths.seedDir, "backup-meta.json"), + JSON.stringify(meta, null, 2) + "\n", + "utf-8", + ); + + // Stop containers for data consistency + console.log("Stopping containers for consistent backup..."); + shell.runSafe(`docker compose -f "${paths.composePath}" stop`); + + // Create tarball β€” include config, data, compose, metadata. Exclude backups dir and .env + const seedBase = basename(paths.seedDir); + const seedParent = dirname(paths.seedDir); + try { + shell.run( + `tar -czf "${backupFile}" -C "${seedParent}" --exclude="${seedBase}/backups" --exclude="${seedBase}/.env" --exclude="${seedBase}/deploy.js" --exclude="${seedBase}/deploy.log" "${seedBase}/config.json" "${seedBase}/backup-meta.json" "${seedBase}/docker-compose.yml" "${seedBase}/web" "${seedBase}/daemon" "${seedBase}/proxy"`, + ); + } catch (err) { + console.error(`Backup failed: ${err}`); + // Restart containers even on failure + shell.runSafe(`docker compose -f "${paths.composePath}" start`); + process.exit(1); + } + + // Clean up metadata file + shell.runSafe(`rm -f "${join(paths.seedDir, "backup-meta.json")}"`); + + // Restart containers + console.log("Restarting containers..."); + shell.runSafe(`docker compose -f "${paths.composePath}" start`); + + const size = + shell.runSafe(`du -h "${backupFile}"`)?.split("\t")[0] ?? "unknown"; + console.log(`\nBackup created: ${backupFile} (${size})`); +} + +async function cmdRestore( + paths: DeployPaths, + shell: ShellRunner, + args: string[], +): Promise { + const backupFile = args[0]; + if (!backupFile) { + console.error("Usage: seed-deploy restore "); + process.exit(1); + } + + try { + await access(backupFile); + } catch { + console.error(`File not found: ${backupFile}`); + process.exit(1); + } + + // Read metadata from tarball without full extraction + let meta: BackupMeta | null = null; + const seedBase = basename(paths.seedDir); + const metaJson = shell.runSafe( + `tar -xzf "${backupFile}" -O "${seedBase}/backup-meta.json" 2>/dev/null`, + ); + if (metaJson) { + try { + meta = JSON.parse(metaJson) as BackupMeta; + } catch { + // malformed metadata β€” proceed without it + } + } + + // Show what we're restoring + p.intro(`Seed Node Restore v${VERSION}`); + if (meta) { + p.note( + [ + `Created: ${meta.timestamp}`, + `Source: ${meta.hostname}`, + `Version: ${meta.version}`, + ].join("\n"), + "Restoring from backup", + ); + } + + const confirmed = await p.confirm({ + message: `This will overwrite all data in ${paths.seedDir}. Continue?`, + }); + if (p.isCancel(confirmed) || !confirmed) { + p.cancel("Restore cancelled."); + console.log("\nRun 'seed-deploy restore ' to try again.\n"); + process.exit(0); + } + + // Stop existing containers + console.log("Stopping existing containers..."); + shell.runSafe(`docker compose -f "${paths.composePath}" down`); + + // Extract backup + console.log("Extracting backup..."); + await ensureSeedDir(paths, shell); + const seedParent = dirname(paths.seedDir); + shell.run(`tar -xzf "${backupFile}" -C "${seedParent}"`); + + // Clean up metadata file from extracted data + shell.runSafe(`rm -f "${join(paths.seedDir, "backup-meta.json")}"`); + + // Restore cron lines from backup metadata + if (meta?.cron && meta.cron.length > 0) { + const existingCron = shell.runSafe("crontab -l 2>/dev/null") ?? ""; + const newCrontab = buildCrontab(existingCron, paths); + try { + shell.run(`echo '${newCrontab}' | crontab -`); + console.log("Cron jobs restored."); + } catch { + console.log("Warning: Could not restore cron jobs."); + } + } + + // Ask if user wants to review configuration + const wantsReview = await p.confirm({ + message: "Would you like to review the configuration before deploying?", + initialValue: false, + }); + + let config: SeedConfig; + if (!p.isCancel(wantsReview) && wantsReview) { + // Load the restored config and run the migration wizard with it pre-filled + const restored = await readConfig(paths); + const asOldInstall: OldInstallInfo = { + workspace: paths.seedDir, + secret: restored.link_secret, + secretConsumed: false, + hostname: restored.domain, + logLevel: restored.compose_envs.LOG_LEVEL, + imageTag: restored.release_channel, + testnet: restored.testnet, + gateway: restored.gateway, + trafficStats: restored.analytics, + }; + config = await runMigrationWizard(asOldInstall, paths, shell); + } else { + config = await readConfig(paths); + } + + // Deploy with the restored (or modified) config + await deploy(config, paths, shell); + + p.outro(`Restore complete! Your Seed node is running.\n${MANAGE_HINT}`); +} + +async function cmdUninstall( + paths: DeployPaths, + shell: ShellRunner, +): Promise { + p.intro(`Seed Node Uninstall v${VERSION}`); + + p.note( + [ + "This will permanently delete:", + ` - All Seed containers`, + ` - All node data at ${paths.seedDir}/ (daemon identity, web data, config)`, + ` - Cron jobs for seed-deploy and seed-cleanup`, + "", + "This action is IRREVERSIBLE.", + ].join("\n"), + "Warning", + ); + + // Offer backup first + const wantsBackup = await p.confirm({ + message: "Would you like to create a backup before uninstalling?", + initialValue: true, + }); + if (!p.isCancel(wantsBackup) && wantsBackup) { + await cmdBackup(paths, shell, []); + } + + const confirmation = await p.text({ + message: 'Type "yes" to confirm uninstallation:', + validate: (v) => { + if (v !== "yes") + return 'Please type "yes" to confirm, or press Ctrl+C to cancel.'; + }, + }); + + if (p.isCancel(confirmation)) { + p.cancel("Uninstall cancelled."); + process.exit(0); + } + + // Stop and remove containers + console.log("Stopping and removing containers..."); + shell.runSafe(`docker compose -f "${paths.composePath}" down`); + + // Remove seed cron lines + const existingCron = shell.runSafe("crontab -l 2>/dev/null") ?? ""; + if ( + existingCron.includes("# seed-deploy") || + existingCron.includes("# seed-cleanup") + ) { + const cleaned = removeSeedCronLines(existingCron); + try { + shell.run(`echo '${cleaned}' | crontab -`); + console.log("Cron jobs removed."); + } catch { + console.log("Warning: Could not remove cron jobs."); + } + } + + // Remove seed directory + console.log(`Removing ${paths.seedDir}...`); + try { + shell.run(`rm -rf "${paths.seedDir}"`); + } catch { + console.log(`Could not remove ${paths.seedDir}. Trying with sudo...`); + shell.run(`sudo rm -rf "${paths.seedDir}"`); + } + + p.outro("Seed node uninstalled."); +} + +// --------------------------------------------------------------------------- +// Main: CLI Dispatch +// --------------------------------------------------------------------------- + +async function main(): Promise { + const { command, args, reconfigure } = parseArgs(); + const paths = makePaths(); + const shell = makeShellRunner(); + + switch (command) { + case "help": + printHelp(); + return; + case "version": + console.log(VERSION); + return; + case "deploy": + return cmdDeploy(paths, shell, reconfigure); + case "stop": + return cmdStop(paths, shell); + case "start": + return cmdStart(paths, shell); + case "restart": + return cmdRestart(paths, shell); + case "status": + return cmdStatus(paths, shell); + case "config": + return cmdConfig(paths); + case "logs": + return cmdLogs(paths, args); + case "cron": + return cmdCron(paths, shell, args); + case "backup": + return cmdBackup(paths, shell, args); + case "restore": + return cmdRestore(paths, shell, args); + case "uninstall": + return cmdUninstall(paths, shell); + } } if (import.meta.main) { diff --git a/ops/dist/deploy.js b/ops/dist/deploy.js index 70ec20cf4..ff70c3b6e 100755 --- a/ops/dist/deploy.js +++ b/ops/dist/deploy.js @@ -1,64 +1,67 @@ #!/usr/bin/env bun // @bun -var Kb=Object.create;var{getPrototypeOf:Sb,defineProperty:M1,getOwnPropertyNames:Yb}=Object;var Qb=Object.prototype.hasOwnProperty;var h=(x,$,B)=>{B=x!=null?Kb(Sb(x)):{};let K=$||!x||!x.__esModule?M1(B,"default",{value:x,enumerable:!0}):B;for(let S of Yb(x))if(!Qb.call(K,S))M1(K,S,{get:()=>x[S],enumerable:!0});return K};var N1=(x,$)=>()=>($||x(($={exports:{}}).exports,$),$.exports);var e=N1((Nx,P1)=>{var s={to(x,$){if(!$)return`\x1B[${x+1}G`;return`\x1B[${$+1};${x+1}H`},move(x,$){let B="";if(x<0)B+=`\x1B[${-x}D`;else if(x>0)B+=`\x1B[${x}C`;if($<0)B+=`\x1B[${-$}A`;else if($>0)B+=`\x1B[${$}B`;return B},up:(x=1)=>`\x1B[${x}A`,down:(x=1)=>`\x1B[${x}B`,forward:(x=1)=>`\x1B[${x}C`,backward:(x=1)=>`\x1B[${x}D`,nextLine:(x=1)=>"\x1B[E".repeat(x),prevLine:(x=1)=>"\x1B[F".repeat(x),left:"\x1B[G",hide:"\x1B[?25l",show:"\x1B[?25h",save:"\x1B7",restore:"\x1B8"},Xb={up:(x=1)=>"\x1B[S".repeat(x),down:(x=1)=>"\x1B[T".repeat(x)},Jb={screen:"\x1B[2J",up:(x=1)=>"\x1B[1J".repeat(x),down:(x=1)=>"\x1B[J".repeat(x),line:"\x1B[2K",lineEnd:"\x1B[K",lineStart:"\x1B[1K",lines(x){let $="";for(let B=0;B{var F=process||{},R1=F.argv||[],l=F.env||{},Zb=!(!!l.NO_COLOR||R1.includes("--no-color"))&&(!!l.FORCE_COLOR||R1.includes("--color")||F.platform==="win32"||(F.stdout||{}).isTTY&&l.TERM!=="dumb"||!!l.CI),qb=(x,$,B=x)=>(K)=>{let S=""+K,Y=S.indexOf($,x.length);return~Y?x+zb(S,$,B,Y)+$:x+S+$},zb=(x,$,B,K)=>{let S="",Y=0;do S+=x.substring(Y,K)+B,Y=K+$.length,K=x.indexOf($,Y);while(~K);return S+x.substring(Y)},T1=(x=Zb)=>{let $=x?qb:()=>String;return{isColorSupported:x,reset:$("\x1B[0m","\x1B[0m"),bold:$("\x1B[1m","\x1B[22m","\x1B[22m\x1B[1m"),dim:$("\x1B[2m","\x1B[22m","\x1B[22m\x1B[2m"),italic:$("\x1B[3m","\x1B[23m"),underline:$("\x1B[4m","\x1B[24m"),inverse:$("\x1B[7m","\x1B[27m"),hidden:$("\x1B[8m","\x1B[28m"),strikethrough:$("\x1B[9m","\x1B[29m"),black:$("\x1B[30m","\x1B[39m"),red:$("\x1B[31m","\x1B[39m"),green:$("\x1B[32m","\x1B[39m"),yellow:$("\x1B[33m","\x1B[39m"),blue:$("\x1B[34m","\x1B[39m"),magenta:$("\x1B[35m","\x1B[39m"),cyan:$("\x1B[36m","\x1B[39m"),white:$("\x1B[37m","\x1B[39m"),gray:$("\x1B[90m","\x1B[39m"),bgBlack:$("\x1B[40m","\x1B[49m"),bgRed:$("\x1B[41m","\x1B[49m"),bgGreen:$("\x1B[42m","\x1B[49m"),bgYellow:$("\x1B[43m","\x1B[49m"),bgBlue:$("\x1B[44m","\x1B[49m"),bgMagenta:$("\x1B[45m","\x1B[49m"),bgCyan:$("\x1B[46m","\x1B[49m"),bgWhite:$("\x1B[47m","\x1B[49m"),blackBright:$("\x1B[90m","\x1B[39m"),redBright:$("\x1B[91m","\x1B[39m"),greenBright:$("\x1B[92m","\x1B[39m"),yellowBright:$("\x1B[93m","\x1B[39m"),blueBright:$("\x1B[94m","\x1B[39m"),magentaBright:$("\x1B[95m","\x1B[39m"),cyanBright:$("\x1B[96m","\x1B[39m"),whiteBright:$("\x1B[97m","\x1B[39m"),bgBlackBright:$("\x1B[100m","\x1B[49m"),bgRedBright:$("\x1B[101m","\x1B[49m"),bgGreenBright:$("\x1B[102m","\x1B[49m"),bgYellowBright:$("\x1B[103m","\x1B[49m"),bgBlueBright:$("\x1B[104m","\x1B[49m"),bgMagentaBright:$("\x1B[105m","\x1B[49m"),bgCyanBright:$("\x1B[106m","\x1B[49m"),bgWhiteBright:$("\x1B[107m","\x1B[49m")}};b1.exports=T1();b1.exports.createColors=T1});import{stripVTControlCharacters as q1}from"util";var T=h(e(),1),f1=h(x1(),1);import{stdin as E1,stdout as w1}from"process";import*as j from"readline";import m1 from"readline";import{WriteStream as Wb}from"tty";function Gb({onlyFirst:x=!1}={}){let $=["[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?(?:\\u0007|\\u001B\\u005C|\\u009C))","(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))"].join("|");return new RegExp($,x?void 0:"g")}var Ib=Gb();function v1(x){if(typeof x!="string")throw TypeError(`Expected a \`string\`, got \`${typeof x}\``);return x.replace(Ib,"")}function d1(x){return x&&x.__esModule&&Object.prototype.hasOwnProperty.call(x,"default")?x.default:x}var g1={exports:{}};(function(x){var $={};x.exports=$,$.eastAsianWidth=function(K){var S=K.charCodeAt(0),Y=K.length==2?K.charCodeAt(1):0,b=S;return 55296<=S&&S<=56319&&56320<=Y&&Y<=57343&&(S&=1023,Y&=1023,b=S<<10|Y,b+=65536),b==12288||65281<=b&&b<=65376||65504<=b&&b<=65510?"F":b==8361||65377<=b&&b<=65470||65474<=b&&b<=65479||65482<=b&&b<=65487||65490<=b&&b<=65495||65498<=b&&b<=65500||65512<=b&&b<=65518?"H":4352<=b&&b<=4447||4515<=b&&b<=4519||4602<=b&&b<=4607||9001<=b&&b<=9002||11904<=b&&b<=11929||11931<=b&&b<=12019||12032<=b&&b<=12245||12272<=b&&b<=12283||12289<=b&&b<=12350||12353<=b&&b<=12438||12441<=b&&b<=12543||12549<=b&&b<=12589||12593<=b&&b<=12686||12688<=b&&b<=12730||12736<=b&&b<=12771||12784<=b&&b<=12830||12832<=b&&b<=12871||12880<=b&&b<=13054||13056<=b&&b<=19903||19968<=b&&b<=42124||42128<=b&&b<=42182||43360<=b&&b<=43388||44032<=b&&b<=55203||55216<=b&&b<=55238||55243<=b&&b<=55291||63744<=b&&b<=64255||65040<=b&&b<=65049||65072<=b&&b<=65106||65108<=b&&b<=65126||65128<=b&&b<=65131||110592<=b&&b<=110593||127488<=b&&b<=127490||127504<=b&&b<=127546||127552<=b&&b<=127560||127568<=b&&b<=127569||131072<=b&&b<=194367||177984<=b&&b<=196605||196608<=b&&b<=262141?"W":32<=b&&b<=126||162<=b&&b<=163||165<=b&&b<=166||b==172||b==175||10214<=b&&b<=10221||10629<=b&&b<=10630?"Na":b==161||b==164||167<=b&&b<=168||b==170||173<=b&&b<=174||176<=b&&b<=180||182<=b&&b<=186||188<=b&&b<=191||b==198||b==208||215<=b&&b<=216||222<=b&&b<=225||b==230||232<=b&&b<=234||236<=b&&b<=237||b==240||242<=b&&b<=243||247<=b&&b<=250||b==252||b==254||b==257||b==273||b==275||b==283||294<=b&&b<=295||b==299||305<=b&&b<=307||b==312||319<=b&&b<=322||b==324||328<=b&&b<=331||b==333||338<=b&&b<=339||358<=b&&b<=359||b==363||b==462||b==464||b==466||b==468||b==470||b==472||b==474||b==476||b==593||b==609||b==708||b==711||713<=b&&b<=715||b==717||b==720||728<=b&&b<=731||b==733||b==735||768<=b&&b<=879||913<=b&&b<=929||931<=b&&b<=937||945<=b&&b<=961||963<=b&&b<=969||b==1025||1040<=b&&b<=1103||b==1105||b==8208||8211<=b&&b<=8214||8216<=b&&b<=8217||8220<=b&&b<=8221||8224<=b&&b<=8226||8228<=b&&b<=8231||b==8240||8242<=b&&b<=8243||b==8245||b==8251||b==8254||b==8308||b==8319||8321<=b&&b<=8324||b==8364||b==8451||b==8453||b==8457||b==8467||b==8470||8481<=b&&b<=8482||b==8486||b==8491||8531<=b&&b<=8532||8539<=b&&b<=8542||8544<=b&&b<=8555||8560<=b&&b<=8569||b==8585||8592<=b&&b<=8601||8632<=b&&b<=8633||b==8658||b==8660||b==8679||b==8704||8706<=b&&b<=8707||8711<=b&&b<=8712||b==8715||b==8719||b==8721||b==8725||b==8730||8733<=b&&b<=8736||b==8739||b==8741||8743<=b&&b<=8748||b==8750||8756<=b&&b<=8759||8764<=b&&b<=8765||b==8776||b==8780||b==8786||8800<=b&&b<=8801||8804<=b&&b<=8807||8810<=b&&b<=8811||8814<=b&&b<=8815||8834<=b&&b<=8835||8838<=b&&b<=8839||b==8853||b==8857||b==8869||b==8895||b==8978||9312<=b&&b<=9449||9451<=b&&b<=9547||9552<=b&&b<=9587||9600<=b&&b<=9615||9618<=b&&b<=9621||9632<=b&&b<=9633||9635<=b&&b<=9641||9650<=b&&b<=9651||9654<=b&&b<=9655||9660<=b&&b<=9661||9664<=b&&b<=9665||9670<=b&&b<=9672||b==9675||9678<=b&&b<=9681||9698<=b&&b<=9701||b==9711||9733<=b&&b<=9734||b==9737||9742<=b&&b<=9743||9748<=b&&b<=9749||b==9756||b==9758||b==9792||b==9794||9824<=b&&b<=9825||9827<=b&&b<=9829||9831<=b&&b<=9834||9836<=b&&b<=9837||b==9839||9886<=b&&b<=9887||9918<=b&&b<=9919||9924<=b&&b<=9933||9935<=b&&b<=9953||b==9955||9960<=b&&b<=9983||b==10045||b==10071||10102<=b&&b<=10111||11093<=b&&b<=11097||12872<=b&&b<=12879||57344<=b&&b<=63743||65024<=b&&b<=65039||b==65533||127232<=b&&b<=127242||127248<=b&&b<=127277||127280<=b&&b<=127337||127344<=b&&b<=127386||917760<=b&&b<=917999||983040<=b&&b<=1048573||1048576<=b&&b<=1114109?"A":"N"},$.characterLength=function(K){var S=this.eastAsianWidth(K);return S=="F"||S=="W"||S=="A"?2:1};function B(K){return K.match(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[^\uD800-\uDFFF]/g)||[]}$.length=function(K){for(var S=B(K),Y=0,b=0;b=S-(q==2?1:0))if(X+q<=Y)b+=z;else break;X+=q}return b}})(g1);var Hb=g1.exports,Vb=d1(Hb),Ob=function(){return/\uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62(?:\uDB40\uDC77\uDB40\uDC6C\uDB40\uDC73|\uDB40\uDC73\uDB40\uDC63\uDB40\uDC74|\uDB40\uDC65\uDB40\uDC6E\uDB40\uDC67)\uDB40\uDC7F|(?:\uD83E\uDDD1\uD83C\uDFFF\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFF\u200D\uD83E\uDD1D\u200D(?:\uD83D[\uDC68\uDC69]))(?:\uD83C[\uDFFB-\uDFFE])|(?:\uD83E\uDDD1\uD83C\uDFFE\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFE\u200D\uD83E\uDD1D\u200D(?:\uD83D[\uDC68\uDC69]))(?:\uD83C[\uDFFB-\uDFFD\uDFFF])|(?:\uD83E\uDDD1\uD83C\uDFFD\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFD\u200D\uD83E\uDD1D\u200D(?:\uD83D[\uDC68\uDC69]))(?:\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])|(?:\uD83E\uDDD1\uD83C\uDFFC\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFC\u200D\uD83E\uDD1D\u200D(?:\uD83D[\uDC68\uDC69]))(?:\uD83C[\uDFFB\uDFFD-\uDFFF])|(?:\uD83E\uDDD1\uD83C\uDFFB\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFB\u200D\uD83E\uDD1D\u200D(?:\uD83D[\uDC68\uDC69]))(?:\uD83C[\uDFFC-\uDFFF])|\uD83D\uDC68(?:\uD83C\uDFFB(?:\u200D(?:\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFF])|\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFF]))|\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFC-\uDFFF])|[\u2695\u2696\u2708]\uFE0F|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD]))?|(?:\uD83C[\uDFFC-\uDFFF])\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFF])|\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFF]))|\u200D(?:\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83D\uDC68|(?:\uD83D[\uDC68\uDC69])\u200D(?:\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67]))|\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67])|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFF\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFE])|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFE\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFD\uDFFF])|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFD\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFC\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB\uDFFD-\uDFFF])|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|(?:\uD83C\uDFFF\u200D[\u2695\u2696\u2708]|\uD83C\uDFFE\u200D[\u2695\u2696\u2708]|\uD83C\uDFFD\u200D[\u2695\u2696\u2708]|\uD83C\uDFFC\u200D[\u2695\u2696\u2708]|\u200D[\u2695\u2696\u2708])\uFE0F|\u200D(?:(?:\uD83D[\uDC68\uDC69])\u200D(?:\uD83D[\uDC66\uDC67])|\uD83D[\uDC66\uDC67])|\uD83C\uDFFF|\uD83C\uDFFE|\uD83C\uDFFD|\uD83C\uDFFC)?|(?:\uD83D\uDC69(?:\uD83C\uDFFB\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D(?:\uD83D[\uDC68\uDC69])|\uD83D[\uDC68\uDC69])|(?:\uD83C[\uDFFC-\uDFFF])\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D(?:\uD83D[\uDC68\uDC69])|\uD83D[\uDC68\uDC69]))|\uD83E\uDDD1(?:\uD83C[\uDFFB-\uDFFF])\u200D\uD83E\uDD1D\u200D\uD83E\uDDD1)(?:\uD83C[\uDFFB-\uDFFF])|\uD83D\uDC69\u200D\uD83D\uDC69\u200D(?:\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67]))|\uD83D\uDC69(?:\u200D(?:\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D(?:\uD83D[\uDC68\uDC69])|\uD83D[\uDC68\uDC69])|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFF\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFE\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFD\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFC\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFB\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD]))|\uD83E\uDDD1(?:\u200D(?:\uD83E\uDD1D\u200D\uD83E\uDDD1|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFF\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFE\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFD\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFC\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFB\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD]))|\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC69\u200D\uD83D\uDC69\u200D(?:\uD83D[\uDC66\uDC67])|\uD83D\uDC69\u200D\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67])|(?:\uD83D\uDC41\uFE0F\u200D\uD83D\uDDE8|\uD83E\uDDD1(?:\uD83C\uDFFF\u200D[\u2695\u2696\u2708]|\uD83C\uDFFE\u200D[\u2695\u2696\u2708]|\uD83C\uDFFD\u200D[\u2695\u2696\u2708]|\uD83C\uDFFC\u200D[\u2695\u2696\u2708]|\uD83C\uDFFB\u200D[\u2695\u2696\u2708]|\u200D[\u2695\u2696\u2708])|\uD83D\uDC69(?:\uD83C\uDFFF\u200D[\u2695\u2696\u2708]|\uD83C\uDFFE\u200D[\u2695\u2696\u2708]|\uD83C\uDFFD\u200D[\u2695\u2696\u2708]|\uD83C\uDFFC\u200D[\u2695\u2696\u2708]|\uD83C\uDFFB\u200D[\u2695\u2696\u2708]|\u200D[\u2695\u2696\u2708])|\uD83D\uDE36\u200D\uD83C\uDF2B|\uD83C\uDFF3\uFE0F\u200D\u26A7|\uD83D\uDC3B\u200D\u2744|(?:(?:\uD83C[\uDFC3\uDFC4\uDFCA]|\uD83D[\uDC6E\uDC70\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6]|\uD83E[\uDD26\uDD35\uDD37-\uDD39\uDD3D\uDD3E\uDDB8\uDDB9\uDDCD-\uDDCF\uDDD4\uDDD6-\uDDDD])(?:\uD83C[\uDFFB-\uDFFF])|\uD83D\uDC6F|\uD83E[\uDD3C\uDDDE\uDDDF])\u200D[\u2640\u2642]|(?:\u26F9|\uD83C[\uDFCB\uDFCC]|\uD83D\uDD75)(?:\uFE0F|\uD83C[\uDFFB-\uDFFF])\u200D[\u2640\u2642]|\uD83C\uDFF4\u200D\u2620|(?:\uD83C[\uDFC3\uDFC4\uDFCA]|\uD83D[\uDC6E\uDC70\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6]|\uD83E[\uDD26\uDD35\uDD37-\uDD39\uDD3D\uDD3E\uDDB8\uDDB9\uDDCD-\uDDCF\uDDD4\uDDD6-\uDDDD])\u200D[\u2640\u2642]|[\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u2328\u23CF\u23ED-\u23EF\u23F1\u23F2\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB\u25FC\u2600-\u2604\u260E\u2611\u2618\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u265F\u2660\u2663\u2665\u2666\u2668\u267B\u267E\u2692\u2694-\u2697\u2699\u269B\u269C\u26A0\u26A7\u26B0\u26B1\u26C8\u26CF\u26D1\u26D3\u26E9\u26F0\u26F1\u26F4\u26F7\u26F8\u2702\u2708\u2709\u270F\u2712\u2714\u2716\u271D\u2721\u2733\u2734\u2744\u2747\u2763\u27A1\u2934\u2935\u2B05-\u2B07\u3030\u303D\u3297\u3299]|\uD83C[\uDD70\uDD71\uDD7E\uDD7F\uDE02\uDE37\uDF21\uDF24-\uDF2C\uDF36\uDF7D\uDF96\uDF97\uDF99-\uDF9B\uDF9E\uDF9F\uDFCD\uDFCE\uDFD4-\uDFDF\uDFF5\uDFF7]|\uD83D[\uDC3F\uDCFD\uDD49\uDD4A\uDD6F\uDD70\uDD73\uDD76-\uDD79\uDD87\uDD8A-\uDD8D\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA\uDECB\uDECD-\uDECF\uDEE0-\uDEE5\uDEE9\uDEF0\uDEF3])\uFE0F|\uD83C\uDFF3\uFE0F\u200D\uD83C\uDF08|\uD83D\uDC69\u200D\uD83D\uDC67|\uD83D\uDC69\u200D\uD83D\uDC66|\uD83D\uDE35\u200D\uD83D\uDCAB|\uD83D\uDE2E\u200D\uD83D\uDCA8|\uD83D\uDC15\u200D\uD83E\uDDBA|\uD83E\uDDD1(?:\uD83C\uDFFF|\uD83C\uDFFE|\uD83C\uDFFD|\uD83C\uDFFC|\uD83C\uDFFB)?|\uD83D\uDC69(?:\uD83C\uDFFF|\uD83C\uDFFE|\uD83C\uDFFD|\uD83C\uDFFC|\uD83C\uDFFB)?|\uD83C\uDDFD\uD83C\uDDF0|\uD83C\uDDF6\uD83C\uDDE6|\uD83C\uDDF4\uD83C\uDDF2|\uD83D\uDC08\u200D\u2B1B|\u2764\uFE0F\u200D(?:\uD83D\uDD25|\uD83E\uDE79)|\uD83D\uDC41\uFE0F|\uD83C\uDFF3\uFE0F|\uD83C\uDDFF(?:\uD83C[\uDDE6\uDDF2\uDDFC])|\uD83C\uDDFE(?:\uD83C[\uDDEA\uDDF9])|\uD83C\uDDFC(?:\uD83C[\uDDEB\uDDF8])|\uD83C\uDDFB(?:\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDEE\uDDF3\uDDFA])|\uD83C\uDDFA(?:\uD83C[\uDDE6\uDDEC\uDDF2\uDDF3\uDDF8\uDDFE\uDDFF])|\uD83C\uDDF9(?:\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDED\uDDEF-\uDDF4\uDDF7\uDDF9\uDDFB\uDDFC\uDDFF])|\uD83C\uDDF8(?:\uD83C[\uDDE6-\uDDEA\uDDEC-\uDDF4\uDDF7-\uDDF9\uDDFB\uDDFD-\uDDFF])|\uD83C\uDDF7(?:\uD83C[\uDDEA\uDDF4\uDDF8\uDDFA\uDDFC])|\uD83C\uDDF5(?:\uD83C[\uDDE6\uDDEA-\uDDED\uDDF0-\uDDF3\uDDF7-\uDDF9\uDDFC\uDDFE])|\uD83C\uDDF3(?:\uD83C[\uDDE6\uDDE8\uDDEA-\uDDEC\uDDEE\uDDF1\uDDF4\uDDF5\uDDF7\uDDFA\uDDFF])|\uD83C\uDDF2(?:\uD83C[\uDDE6\uDDE8-\uDDED\uDDF0-\uDDFF])|\uD83C\uDDF1(?:\uD83C[\uDDE6-\uDDE8\uDDEE\uDDF0\uDDF7-\uDDFB\uDDFE])|\uD83C\uDDF0(?:\uD83C[\uDDEA\uDDEC-\uDDEE\uDDF2\uDDF3\uDDF5\uDDF7\uDDFC\uDDFE\uDDFF])|\uD83C\uDDEF(?:\uD83C[\uDDEA\uDDF2\uDDF4\uDDF5])|\uD83C\uDDEE(?:\uD83C[\uDDE8-\uDDEA\uDDF1-\uDDF4\uDDF6-\uDDF9])|\uD83C\uDDED(?:\uD83C[\uDDF0\uDDF2\uDDF3\uDDF7\uDDF9\uDDFA])|\uD83C\uDDEC(?:\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEE\uDDF1-\uDDF3\uDDF5-\uDDFA\uDDFC\uDDFE])|\uD83C\uDDEB(?:\uD83C[\uDDEE-\uDDF0\uDDF2\uDDF4\uDDF7])|\uD83C\uDDEA(?:\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDED\uDDF7-\uDDFA])|\uD83C\uDDE9(?:\uD83C[\uDDEA\uDDEC\uDDEF\uDDF0\uDDF2\uDDF4\uDDFF])|\uD83C\uDDE8(?:\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDEE\uDDF0-\uDDF5\uDDF7\uDDFA-\uDDFF])|\uD83C\uDDE7(?:\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEF\uDDF1-\uDDF4\uDDF6-\uDDF9\uDDFB\uDDFC\uDDFE\uDDFF])|\uD83C\uDDE6(?:\uD83C[\uDDE8-\uDDEC\uDDEE\uDDF1\uDDF2\uDDF4\uDDF6-\uDDFA\uDDFC\uDDFD\uDDFF])|[#\*0-9]\uFE0F\u20E3|\u2764\uFE0F|(?:\uD83C[\uDFC3\uDFC4\uDFCA]|\uD83D[\uDC6E\uDC70\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6]|\uD83E[\uDD26\uDD35\uDD37-\uDD39\uDD3D\uDD3E\uDDB8\uDDB9\uDDCD-\uDDCF\uDDD4\uDDD6-\uDDDD])(?:\uD83C[\uDFFB-\uDFFF])|(?:\u26F9|\uD83C[\uDFCB\uDFCC]|\uD83D\uDD75)(?:\uFE0F|\uD83C[\uDFFB-\uDFFF])|\uD83C\uDFF4|(?:[\u270A\u270B]|\uD83C[\uDF85\uDFC2\uDFC7]|\uD83D[\uDC42\uDC43\uDC46-\uDC50\uDC66\uDC67\uDC6B-\uDC6D\uDC72\uDC74-\uDC76\uDC78\uDC7C\uDC83\uDC85\uDC8F\uDC91\uDCAA\uDD7A\uDD95\uDD96\uDE4C\uDE4F\uDEC0\uDECC]|\uD83E[\uDD0C\uDD0F\uDD18-\uDD1C\uDD1E\uDD1F\uDD30-\uDD34\uDD36\uDD77\uDDB5\uDDB6\uDDBB\uDDD2\uDDD3\uDDD5])(?:\uD83C[\uDFFB-\uDFFF])|(?:[\u261D\u270C\u270D]|\uD83D[\uDD74\uDD90])(?:\uFE0F|\uD83C[\uDFFB-\uDFFF])|[\u270A\u270B]|\uD83C[\uDF85\uDFC2\uDFC7]|\uD83D[\uDC08\uDC15\uDC3B\uDC42\uDC43\uDC46-\uDC50\uDC66\uDC67\uDC6B-\uDC6D\uDC72\uDC74-\uDC76\uDC78\uDC7C\uDC83\uDC85\uDC8F\uDC91\uDCAA\uDD7A\uDD95\uDD96\uDE2E\uDE35\uDE36\uDE4C\uDE4F\uDEC0\uDECC]|\uD83E[\uDD0C\uDD0F\uDD18-\uDD1C\uDD1E\uDD1F\uDD30-\uDD34\uDD36\uDD77\uDDB5\uDDB6\uDDBB\uDDD2\uDDD3\uDDD5]|\uD83C[\uDFC3\uDFC4\uDFCA]|\uD83D[\uDC6E\uDC70\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6]|\uD83E[\uDD26\uDD35\uDD37-\uDD39\uDD3D\uDD3E\uDDB8\uDDB9\uDDCD-\uDDCF\uDDD4\uDDD6-\uDDDD]|\uD83D\uDC6F|\uD83E[\uDD3C\uDDDE\uDDDF]|[\u231A\u231B\u23E9-\u23EC\u23F0\u23F3\u25FD\u25FE\u2614\u2615\u2648-\u2653\u267F\u2693\u26A1\u26AA\u26AB\u26BD\u26BE\u26C4\u26C5\u26CE\u26D4\u26EA\u26F2\u26F3\u26F5\u26FA\u26FD\u2705\u2728\u274C\u274E\u2753-\u2755\u2757\u2795-\u2797\u27B0\u27BF\u2B1B\u2B1C\u2B50\u2B55]|\uD83C[\uDC04\uDCCF\uDD8E\uDD91-\uDD9A\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF7C\uDF7E-\uDF84\uDF86-\uDF93\uDFA0-\uDFC1\uDFC5\uDFC6\uDFC8\uDFC9\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF8-\uDFFF]|\uD83D[\uDC00-\uDC07\uDC09-\uDC14\uDC16-\uDC3A\uDC3C-\uDC3E\uDC40\uDC44\uDC45\uDC51-\uDC65\uDC6A\uDC79-\uDC7B\uDC7D-\uDC80\uDC84\uDC88-\uDC8E\uDC90\uDC92-\uDCA9\uDCAB-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDDA4\uDDFB-\uDE2D\uDE2F-\uDE34\uDE37-\uDE44\uDE48-\uDE4A\uDE80-\uDEA2\uDEA4-\uDEB3\uDEB7-\uDEBF\uDEC1-\uDEC5\uDED0-\uDED2\uDED5-\uDED7\uDEEB\uDEEC\uDEF4-\uDEFC\uDFE0-\uDFEB]|\uD83E[\uDD0D\uDD0E\uDD10-\uDD17\uDD1D\uDD20-\uDD25\uDD27-\uDD2F\uDD3A\uDD3F-\uDD45\uDD47-\uDD76\uDD78\uDD7A-\uDDB4\uDDB7\uDDBA\uDDBC-\uDDCB\uDDD0\uDDE0-\uDDFF\uDE70-\uDE74\uDE78-\uDE7A\uDE80-\uDE86\uDE90-\uDEA8\uDEB0-\uDEB6\uDEC0-\uDEC2\uDED0-\uDED6]|(?:[\u231A\u231B\u23E9-\u23EC\u23F0\u23F3\u25FD\u25FE\u2614\u2615\u2648-\u2653\u267F\u2693\u26A1\u26AA\u26AB\u26BD\u26BE\u26C4\u26C5\u26CE\u26D4\u26EA\u26F2\u26F3\u26F5\u26FA\u26FD\u2705\u270A\u270B\u2728\u274C\u274E\u2753-\u2755\u2757\u2795-\u2797\u27B0\u27BF\u2B1B\u2B1C\u2B50\u2B55]|\uD83C[\uDC04\uDCCF\uDD8E\uDD91-\uDD9A\uDDE6-\uDDFF\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF7C\uDF7E-\uDF93\uDFA0-\uDFCA\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF4\uDFF8-\uDFFF]|\uD83D[\uDC00-\uDC3E\uDC40\uDC42-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDD7A\uDD95\uDD96\uDDA4\uDDFB-\uDE4F\uDE80-\uDEC5\uDECC\uDED0-\uDED2\uDED5-\uDED7\uDEEB\uDEEC\uDEF4-\uDEFC\uDFE0-\uDFEB]|\uD83E[\uDD0C-\uDD3A\uDD3C-\uDD45\uDD47-\uDD78\uDD7A-\uDDCB\uDDCD-\uDDFF\uDE70-\uDE74\uDE78-\uDE7A\uDE80-\uDE86\uDE90-\uDEA8\uDEB0-\uDEB6\uDEC0-\uDEC2\uDED0-\uDED6])|(?:[#\*0-9\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u231A\u231B\u2328\u23CF\u23E9-\u23F3\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB-\u25FE\u2600-\u2604\u260E\u2611\u2614\u2615\u2618\u261D\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u2648-\u2653\u265F\u2660\u2663\u2665\u2666\u2668\u267B\u267E\u267F\u2692-\u2697\u2699\u269B\u269C\u26A0\u26A1\u26A7\u26AA\u26AB\u26B0\u26B1\u26BD\u26BE\u26C4\u26C5\u26C8\u26CE\u26CF\u26D1\u26D3\u26D4\u26E9\u26EA\u26F0-\u26F5\u26F7-\u26FA\u26FD\u2702\u2705\u2708-\u270D\u270F\u2712\u2714\u2716\u271D\u2721\u2728\u2733\u2734\u2744\u2747\u274C\u274E\u2753-\u2755\u2757\u2763\u2764\u2795-\u2797\u27A1\u27B0\u27BF\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B50\u2B55\u3030\u303D\u3297\u3299]|\uD83C[\uDC04\uDCCF\uDD70\uDD71\uDD7E\uDD7F\uDD8E\uDD91-\uDD9A\uDDE6-\uDDFF\uDE01\uDE02\uDE1A\uDE2F\uDE32-\uDE3A\uDE50\uDE51\uDF00-\uDF21\uDF24-\uDF93\uDF96\uDF97\uDF99-\uDF9B\uDF9E-\uDFF0\uDFF3-\uDFF5\uDFF7-\uDFFF]|\uD83D[\uDC00-\uDCFD\uDCFF-\uDD3D\uDD49-\uDD4E\uDD50-\uDD67\uDD6F\uDD70\uDD73-\uDD7A\uDD87\uDD8A-\uDD8D\uDD90\uDD95\uDD96\uDDA4\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA-\uDE4F\uDE80-\uDEC5\uDECB-\uDED2\uDED5-\uDED7\uDEE0-\uDEE5\uDEE9\uDEEB\uDEEC\uDEF0\uDEF3-\uDEFC\uDFE0-\uDFEB]|\uD83E[\uDD0C-\uDD3A\uDD3C-\uDD45\uDD47-\uDD78\uDD7A-\uDDCB\uDDCD-\uDDFF\uDE70-\uDE74\uDE78-\uDE7A\uDE80-\uDE86\uDE90-\uDEA8\uDEB0-\uDEB6\uDEC0-\uDEC2\uDED0-\uDED6])\uFE0F|(?:[\u261D\u26F9\u270A-\u270D]|\uD83C[\uDF85\uDFC2-\uDFC4\uDFC7\uDFCA-\uDFCC]|\uD83D[\uDC42\uDC43\uDC46-\uDC50\uDC66-\uDC78\uDC7C\uDC81-\uDC83\uDC85-\uDC87\uDC8F\uDC91\uDCAA\uDD74\uDD75\uDD7A\uDD90\uDD95\uDD96\uDE45-\uDE47\uDE4B-\uDE4F\uDEA3\uDEB4-\uDEB6\uDEC0\uDECC]|\uD83E[\uDD0C\uDD0F\uDD18-\uDD1F\uDD26\uDD30-\uDD39\uDD3C-\uDD3E\uDD77\uDDB5\uDDB6\uDDB8\uDDB9\uDDBB\uDDCD-\uDDCF\uDDD1-\uDDDD])/g},Ab=d1(Ob);function v(x,$={}){if(typeof x!="string"||x.length===0||($={ambiguousIsNarrow:!0,...$},x=v1(x),x.length===0))return 0;x=x.replace(Ab()," ");let B=$.ambiguousIsNarrow?1:2,K=0;for(let S of x){let Y=S.codePointAt(0);if(Y<=31||Y>=127&&Y<=159||Y>=768&&Y<=879)continue;switch(Vb.eastAsianWidth(S)){case"F":case"W":K+=2;break;case"A":K+=B;break;default:K+=1}}return K}var $1=10,L1=(x=0)=>($)=>`\x1B[${$+x}m`,_1=(x=0)=>($)=>`\x1B[${38+x};5;${$}m`,U1=(x=0)=>($,B,K)=>`\x1B[${38+x};2;${$};${B};${K}m`,G={modifier:{reset:[0,0],bold:[1,22],dim:[2,22],italic:[3,23],underline:[4,24],overline:[53,55],inverse:[7,27],hidden:[8,28],strikethrough:[9,29]},color:{black:[30,39],red:[31,39],green:[32,39],yellow:[33,39],blue:[34,39],magenta:[35,39],cyan:[36,39],white:[37,39],blackBright:[90,39],gray:[90,39],grey:[90,39],redBright:[91,39],greenBright:[92,39],yellowBright:[93,39],blueBright:[94,39],magentaBright:[95,39],cyanBright:[96,39],whiteBright:[97,39]},bgColor:{bgBlack:[40,49],bgRed:[41,49],bgGreen:[42,49],bgYellow:[43,49],bgBlue:[44,49],bgMagenta:[45,49],bgCyan:[46,49],bgWhite:[47,49],bgBlackBright:[100,49],bgGray:[100,49],bgGrey:[100,49],bgRedBright:[101,49],bgGreenBright:[102,49],bgYellowBright:[103,49],bgBlueBright:[104,49],bgMagentaBright:[105,49],bgCyanBright:[106,49],bgWhiteBright:[107,49]}};Object.keys(G.modifier);var Mb=Object.keys(G.color),Nb=Object.keys(G.bgColor);[...Mb,...Nb];function Pb(){let x=new Map;for(let[$,B]of Object.entries(G)){for(let[K,S]of Object.entries(B))G[K]={open:`\x1B[${S[0]}m`,close:`\x1B[${S[1]}m`},B[K]=G[K],x.set(S[0],S[1]);Object.defineProperty(G,$,{value:B,enumerable:!1})}return Object.defineProperty(G,"codes",{value:x,enumerable:!1}),G.color.close="\x1B[39m",G.bgColor.close="\x1B[49m",G.color.ansi=L1(),G.color.ansi256=_1(),G.color.ansi16m=U1(),G.bgColor.ansi=L1($1),G.bgColor.ansi256=_1($1),G.bgColor.ansi16m=U1($1),Object.defineProperties(G,{rgbToAnsi256:{value:($,B,K)=>$===B&&B===K?$<8?16:$>248?231:Math.round(($-8)/247*24)+232:16+36*Math.round($/255*5)+6*Math.round(B/255*5)+Math.round(K/255*5),enumerable:!1},hexToRgb:{value:($)=>{let B=/[a-f\d]{6}|[a-f\d]{3}/i.exec($.toString(16));if(!B)return[0,0,0];let[K]=B;K.length===3&&(K=[...K].map((Y)=>Y+Y).join(""));let S=Number.parseInt(K,16);return[S>>16&255,S>>8&255,S&255]},enumerable:!1},hexToAnsi256:{value:($)=>G.rgbToAnsi256(...G.hexToRgb($)),enumerable:!1},ansi256ToAnsi:{value:($)=>{if($<8)return 30+$;if($<16)return 90+($-8);let B,K,S;if($>=232)B=(($-232)*10+8)/255,K=B,S=B;else{$-=16;let X=$%36;B=Math.floor($/36)/5,K=Math.floor(X/6)/5,S=X%6/5}let Y=Math.max(B,K,S)*2;if(Y===0)return 30;let b=30+(Math.round(S)<<2|Math.round(K)<<1|Math.round(B));return Y===2&&(b+=60),b},enumerable:!1},rgbToAnsi:{value:($,B,K)=>G.ansi256ToAnsi(G.rgbToAnsi256($,B,K)),enumerable:!1},hexToAnsi:{value:($)=>G.ansi256ToAnsi(G.hexToAnsi256($)),enumerable:!1}}),G}var Rb=Pb(),o=new Set(["\x1B","\x9B"]),Tb=39,S1="\x07",p1="[",mb="]",h1="m",Y1=`${mb}8;;`,C1=(x)=>`${o.values().next().value}${p1}${x}${h1}`,j1=(x)=>`${o.values().next().value}${Y1}${x}${S1}`,Lb=(x)=>x.split(" ").map(($)=>v($)),B1=(x,$,B)=>{let K=[...$],S=!1,Y=!1,b=v(v1(x[x.length-1]));for(let[X,J]of K.entries()){let Z=v(J);if(b+Z<=B?x[x.length-1]+=J:(x.push(J),b=0),o.has(J)&&(S=!0,Y=K.slice(X+1).join("").startsWith(Y1)),S){Y?J===S1&&(S=!1,Y=!1):J===h1&&(S=!1);continue}b+=Z,b===B&&X0&&x.length>1&&(x[x.length-2]+=x.pop())},_b=(x)=>{let $=x.split(" "),B=$.length;for(;B>0&&!(v($[B-1])>0);)B--;return B===$.length?x:$.slice(0,B).join(" ")+$.slice(B).join("")},Ub=(x,$,B={})=>{if(B.trim!==!1&&x.trim()==="")return"";let K="",S,Y,b=Lb(x),X=[""];for(let[Z,z]of x.split(" ").entries()){B.trim!==!1&&(X[X.length-1]=X[X.length-1].trimStart());let q=v(X[X.length-1]);if(Z!==0&&(q>=$&&(B.wordWrap===!1||B.trim===!1)&&(X.push(""),q=0),(q>0||B.trim===!1)&&(X[X.length-1]+=" ",q++)),B.hard&&b[Z]>$){let H=$-q,N=1+Math.floor((b[Z]-H-1)/$);Math.floor((b[Z]-1)/$)$&&q>0&&b[Z]>0){if(B.wordWrap===!1&&q<$){B1(X,z,$);continue}X.push("")}if(q+b[Z]>$&&B.wordWrap===!1){B1(X,z,$);continue}X[X.length-1]+=z}B.trim!==!1&&(X=X.map((Z)=>_b(Z)));let J=[...X.join(` -`)];for(let[Z,z]of J.entries()){if(K+=z,o.has(z)){let{groups:H}=new RegExp(`(?:\\${p1}(?\\d+)m|\\${Y1}(?.*)${S1})`).exec(J.slice(Z).join(""))||{groups:{}};if(H.code!==void 0){let N=Number.parseFloat(H.code);S=N===Tb?void 0:N}else H.uri!==void 0&&(Y=H.uri.length===0?void 0:H.uri)}let q=Rb.codes.get(Number(S));J[Z+1]===` -`?(Y&&(K+=j1("")),S&&q&&(K+=C1(q))):z===` -`&&(S&&q&&(K+=C1(S)),Y&&(K+=j1(Y)))}return K};function k1(x,$,B){return String(x).normalize().replace(/\r\n/g,` +var Lb=Object.create;var{getPrototypeOf:Ub,defineProperty:C1,getOwnPropertyNames:_b}=Object;var Nb=Object.prototype.hasOwnProperty;var l=($,K,Y)=>{Y=$!=null?Lb(Ub($)):{};let Q=K||!$||!$.__esModule?C1(Y,"default",{value:$,enumerable:!0}):Y;for(let X of _b($))if(!Nb.call(Q,X))C1(Q,X,{get:()=>$[X],enumerable:!0});return Q};var E1=($,K)=>()=>(K||$((K={exports:{}}).exports,K),K.exports);var B1=E1((h9,m1)=>{var q1={to($,K){if(!K)return`\x1B[${$+1}G`;return`\x1B[${K+1};${$+1}H`},move($,K){let Y="";if($<0)Y+=`\x1B[${-$}D`;else if($>0)Y+=`\x1B[${$}C`;if(K<0)Y+=`\x1B[${-K}A`;else if(K>0)Y+=`\x1B[${K}B`;return Y},up:($=1)=>`\x1B[${$}A`,down:($=1)=>`\x1B[${$}B`,forward:($=1)=>`\x1B[${$}C`,backward:($=1)=>`\x1B[${$}D`,nextLine:($=1)=>"\x1B[E".repeat($),prevLine:($=1)=>"\x1B[F".repeat($),left:"\x1B[G",hide:"\x1B[?25l",show:"\x1B[?25h",save:"\x1B7",restore:"\x1B8"},jb={up:($=1)=>"\x1B[S".repeat($),down:($=1)=>"\x1B[T".repeat($)},Rb={screen:"\x1B[2J",up:($=1)=>"\x1B[1J".repeat($),down:($=1)=>"\x1B[J".repeat($),line:"\x1B[2K",lineEnd:"\x1B[K",lineStart:"\x1B[1K",lines($){let K="";for(let Y=0;Y<$;Y++)K+=this.line+(Y<$-1?q1.up():"");if($)K+=q1.left;return K}};m1.exports={cursor:q1,scroll:jb,erase:Rb,beep:"\x07"}});var W1=E1((D9,J1)=>{var i=process||{},v1=i.argv||[],n=i.env||{},kb=!(!!n.NO_COLOR||v1.includes("--no-color"))&&(!!n.FORCE_COLOR||v1.includes("--color")||i.platform==="win32"||(i.stdout||{}).isTTY&&n.TERM!=="dumb"||!!n.CI),yb=($,K,Y=$)=>(Q)=>{let X=""+Q,x=X.indexOf(K,$.length);return~x?$+wb(X,K,Y,x)+K:$+X+K},wb=($,K,Y,Q)=>{let X="",x=0;do X+=$.substring(x,Q)+Y,x=Q+K.length,Q=$.indexOf(K,x);while(~Q);return X+$.substring(x)},f1=($=kb)=>{let K=$?yb:()=>String;return{isColorSupported:$,reset:K("\x1B[0m","\x1B[0m"),bold:K("\x1B[1m","\x1B[22m","\x1B[22m\x1B[1m"),dim:K("\x1B[2m","\x1B[22m","\x1B[22m\x1B[2m"),italic:K("\x1B[3m","\x1B[23m"),underline:K("\x1B[4m","\x1B[24m"),inverse:K("\x1B[7m","\x1B[27m"),hidden:K("\x1B[8m","\x1B[28m"),strikethrough:K("\x1B[9m","\x1B[29m"),black:K("\x1B[30m","\x1B[39m"),red:K("\x1B[31m","\x1B[39m"),green:K("\x1B[32m","\x1B[39m"),yellow:K("\x1B[33m","\x1B[39m"),blue:K("\x1B[34m","\x1B[39m"),magenta:K("\x1B[35m","\x1B[39m"),cyan:K("\x1B[36m","\x1B[39m"),white:K("\x1B[37m","\x1B[39m"),gray:K("\x1B[90m","\x1B[39m"),bgBlack:K("\x1B[40m","\x1B[49m"),bgRed:K("\x1B[41m","\x1B[49m"),bgGreen:K("\x1B[42m","\x1B[49m"),bgYellow:K("\x1B[43m","\x1B[49m"),bgBlue:K("\x1B[44m","\x1B[49m"),bgMagenta:K("\x1B[45m","\x1B[49m"),bgCyan:K("\x1B[46m","\x1B[49m"),bgWhite:K("\x1B[47m","\x1B[49m"),blackBright:K("\x1B[90m","\x1B[39m"),redBright:K("\x1B[91m","\x1B[39m"),greenBright:K("\x1B[92m","\x1B[39m"),yellowBright:K("\x1B[93m","\x1B[39m"),blueBright:K("\x1B[94m","\x1B[39m"),magentaBright:K("\x1B[95m","\x1B[39m"),cyanBright:K("\x1B[96m","\x1B[39m"),whiteBright:K("\x1B[97m","\x1B[39m"),bgBlackBright:K("\x1B[100m","\x1B[49m"),bgRedBright:K("\x1B[101m","\x1B[49m"),bgGreenBright:K("\x1B[102m","\x1B[49m"),bgYellowBright:K("\x1B[103m","\x1B[49m"),bgBlueBright:K("\x1B[104m","\x1B[49m"),bgMagentaBright:K("\x1B[105m","\x1B[49m"),bgCyanBright:K("\x1B[106m","\x1B[49m"),bgWhiteBright:K("\x1B[107m","\x1B[49m")}};J1.exports=f1();J1.exports.createColors=f1});import{stripVTControlCharacters as S1}from"util";var _=l(B1(),1),l1=l(W1(),1);import{stdin as h1,stdout as D1}from"process";import*as m from"readline";import F1 from"readline";import{WriteStream as Cb}from"tty";function Eb({onlyFirst:$=!1}={}){let K=["[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?(?:\\u0007|\\u001B\\u005C|\\u009C))","(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))"].join("|");return new RegExp(K,$?void 0:"g")}var mb=Eb();function n1($){if(typeof $!="string")throw TypeError(`Expected a \`string\`, got \`${typeof $}\``);return $.replace(mb,"")}function i1($){return $&&$.__esModule&&Object.prototype.hasOwnProperty.call($,"default")?$.default:$}var a1={exports:{}};(function($){var K={};$.exports=K,K.eastAsianWidth=function(Q){var X=Q.charCodeAt(0),x=Q.length==2?Q.charCodeAt(1):0,b=X;return 55296<=X&&X<=56319&&56320<=x&&x<=57343&&(X&=1023,x&=1023,b=X<<10|x,b+=65536),b==12288||65281<=b&&b<=65376||65504<=b&&b<=65510?"F":b==8361||65377<=b&&b<=65470||65474<=b&&b<=65479||65482<=b&&b<=65487||65490<=b&&b<=65495||65498<=b&&b<=65500||65512<=b&&b<=65518?"H":4352<=b&&b<=4447||4515<=b&&b<=4519||4602<=b&&b<=4607||9001<=b&&b<=9002||11904<=b&&b<=11929||11931<=b&&b<=12019||12032<=b&&b<=12245||12272<=b&&b<=12283||12289<=b&&b<=12350||12353<=b&&b<=12438||12441<=b&&b<=12543||12549<=b&&b<=12589||12593<=b&&b<=12686||12688<=b&&b<=12730||12736<=b&&b<=12771||12784<=b&&b<=12830||12832<=b&&b<=12871||12880<=b&&b<=13054||13056<=b&&b<=19903||19968<=b&&b<=42124||42128<=b&&b<=42182||43360<=b&&b<=43388||44032<=b&&b<=55203||55216<=b&&b<=55238||55243<=b&&b<=55291||63744<=b&&b<=64255||65040<=b&&b<=65049||65072<=b&&b<=65106||65108<=b&&b<=65126||65128<=b&&b<=65131||110592<=b&&b<=110593||127488<=b&&b<=127490||127504<=b&&b<=127546||127552<=b&&b<=127560||127568<=b&&b<=127569||131072<=b&&b<=194367||177984<=b&&b<=196605||196608<=b&&b<=262141?"W":32<=b&&b<=126||162<=b&&b<=163||165<=b&&b<=166||b==172||b==175||10214<=b&&b<=10221||10629<=b&&b<=10630?"Na":b==161||b==164||167<=b&&b<=168||b==170||173<=b&&b<=174||176<=b&&b<=180||182<=b&&b<=186||188<=b&&b<=191||b==198||b==208||215<=b&&b<=216||222<=b&&b<=225||b==230||232<=b&&b<=234||236<=b&&b<=237||b==240||242<=b&&b<=243||247<=b&&b<=250||b==252||b==254||b==257||b==273||b==275||b==283||294<=b&&b<=295||b==299||305<=b&&b<=307||b==312||319<=b&&b<=322||b==324||328<=b&&b<=331||b==333||338<=b&&b<=339||358<=b&&b<=359||b==363||b==462||b==464||b==466||b==468||b==470||b==472||b==474||b==476||b==593||b==609||b==708||b==711||713<=b&&b<=715||b==717||b==720||728<=b&&b<=731||b==733||b==735||768<=b&&b<=879||913<=b&&b<=929||931<=b&&b<=937||945<=b&&b<=961||963<=b&&b<=969||b==1025||1040<=b&&b<=1103||b==1105||b==8208||8211<=b&&b<=8214||8216<=b&&b<=8217||8220<=b&&b<=8221||8224<=b&&b<=8226||8228<=b&&b<=8231||b==8240||8242<=b&&b<=8243||b==8245||b==8251||b==8254||b==8308||b==8319||8321<=b&&b<=8324||b==8364||b==8451||b==8453||b==8457||b==8467||b==8470||8481<=b&&b<=8482||b==8486||b==8491||8531<=b&&b<=8532||8539<=b&&b<=8542||8544<=b&&b<=8555||8560<=b&&b<=8569||b==8585||8592<=b&&b<=8601||8632<=b&&b<=8633||b==8658||b==8660||b==8679||b==8704||8706<=b&&b<=8707||8711<=b&&b<=8712||b==8715||b==8719||b==8721||b==8725||b==8730||8733<=b&&b<=8736||b==8739||b==8741||8743<=b&&b<=8748||b==8750||8756<=b&&b<=8759||8764<=b&&b<=8765||b==8776||b==8780||b==8786||8800<=b&&b<=8801||8804<=b&&b<=8807||8810<=b&&b<=8811||8814<=b&&b<=8815||8834<=b&&b<=8835||8838<=b&&b<=8839||b==8853||b==8857||b==8869||b==8895||b==8978||9312<=b&&b<=9449||9451<=b&&b<=9547||9552<=b&&b<=9587||9600<=b&&b<=9615||9618<=b&&b<=9621||9632<=b&&b<=9633||9635<=b&&b<=9641||9650<=b&&b<=9651||9654<=b&&b<=9655||9660<=b&&b<=9661||9664<=b&&b<=9665||9670<=b&&b<=9672||b==9675||9678<=b&&b<=9681||9698<=b&&b<=9701||b==9711||9733<=b&&b<=9734||b==9737||9742<=b&&b<=9743||9748<=b&&b<=9749||b==9756||b==9758||b==9792||b==9794||9824<=b&&b<=9825||9827<=b&&b<=9829||9831<=b&&b<=9834||9836<=b&&b<=9837||b==9839||9886<=b&&b<=9887||9918<=b&&b<=9919||9924<=b&&b<=9933||9935<=b&&b<=9953||b==9955||9960<=b&&b<=9983||b==10045||b==10071||10102<=b&&b<=10111||11093<=b&&b<=11097||12872<=b&&b<=12879||57344<=b&&b<=63743||65024<=b&&b<=65039||b==65533||127232<=b&&b<=127242||127248<=b&&b<=127277||127280<=b&&b<=127337||127344<=b&&b<=127386||917760<=b&&b<=917999||983040<=b&&b<=1048573||1048576<=b&&b<=1114109?"A":"N"},K.characterLength=function(Q){var X=this.eastAsianWidth(Q);return X=="F"||X=="W"||X=="A"?2:1};function Y(Q){return Q.match(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[^\uD800-\uDFFF]/g)||[]}K.length=function(Q){for(var X=Y(Q),x=0,b=0;b=X-(B==2?1:0))if(Z+B<=x)b+=J;else break;Z+=B}return b}})(a1);var vb=a1.exports,fb=i1(vb),Fb=function(){return/\uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62(?:\uDB40\uDC77\uDB40\uDC6C\uDB40\uDC73|\uDB40\uDC73\uDB40\uDC63\uDB40\uDC74|\uDB40\uDC65\uDB40\uDC6E\uDB40\uDC67)\uDB40\uDC7F|(?:\uD83E\uDDD1\uD83C\uDFFF\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFF\u200D\uD83E\uDD1D\u200D(?:\uD83D[\uDC68\uDC69]))(?:\uD83C[\uDFFB-\uDFFE])|(?:\uD83E\uDDD1\uD83C\uDFFE\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFE\u200D\uD83E\uDD1D\u200D(?:\uD83D[\uDC68\uDC69]))(?:\uD83C[\uDFFB-\uDFFD\uDFFF])|(?:\uD83E\uDDD1\uD83C\uDFFD\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFD\u200D\uD83E\uDD1D\u200D(?:\uD83D[\uDC68\uDC69]))(?:\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])|(?:\uD83E\uDDD1\uD83C\uDFFC\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFC\u200D\uD83E\uDD1D\u200D(?:\uD83D[\uDC68\uDC69]))(?:\uD83C[\uDFFB\uDFFD-\uDFFF])|(?:\uD83E\uDDD1\uD83C\uDFFB\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFB\u200D\uD83E\uDD1D\u200D(?:\uD83D[\uDC68\uDC69]))(?:\uD83C[\uDFFC-\uDFFF])|\uD83D\uDC68(?:\uD83C\uDFFB(?:\u200D(?:\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFF])|\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFF]))|\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFC-\uDFFF])|[\u2695\u2696\u2708]\uFE0F|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD]))?|(?:\uD83C[\uDFFC-\uDFFF])\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFF])|\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFF]))|\u200D(?:\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83D\uDC68|(?:\uD83D[\uDC68\uDC69])\u200D(?:\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67]))|\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67])|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFF\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFE])|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFE\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFD\uDFFF])|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFD\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFC\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB\uDFFD-\uDFFF])|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|(?:\uD83C\uDFFF\u200D[\u2695\u2696\u2708]|\uD83C\uDFFE\u200D[\u2695\u2696\u2708]|\uD83C\uDFFD\u200D[\u2695\u2696\u2708]|\uD83C\uDFFC\u200D[\u2695\u2696\u2708]|\u200D[\u2695\u2696\u2708])\uFE0F|\u200D(?:(?:\uD83D[\uDC68\uDC69])\u200D(?:\uD83D[\uDC66\uDC67])|\uD83D[\uDC66\uDC67])|\uD83C\uDFFF|\uD83C\uDFFE|\uD83C\uDFFD|\uD83C\uDFFC)?|(?:\uD83D\uDC69(?:\uD83C\uDFFB\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D(?:\uD83D[\uDC68\uDC69])|\uD83D[\uDC68\uDC69])|(?:\uD83C[\uDFFC-\uDFFF])\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D(?:\uD83D[\uDC68\uDC69])|\uD83D[\uDC68\uDC69]))|\uD83E\uDDD1(?:\uD83C[\uDFFB-\uDFFF])\u200D\uD83E\uDD1D\u200D\uD83E\uDDD1)(?:\uD83C[\uDFFB-\uDFFF])|\uD83D\uDC69\u200D\uD83D\uDC69\u200D(?:\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67]))|\uD83D\uDC69(?:\u200D(?:\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D(?:\uD83D[\uDC68\uDC69])|\uD83D[\uDC68\uDC69])|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFF\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFE\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFD\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFC\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFB\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD]))|\uD83E\uDDD1(?:\u200D(?:\uD83E\uDD1D\u200D\uD83E\uDDD1|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFF\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFE\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFD\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFC\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFB\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD]))|\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC69\u200D\uD83D\uDC69\u200D(?:\uD83D[\uDC66\uDC67])|\uD83D\uDC69\u200D\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67])|(?:\uD83D\uDC41\uFE0F\u200D\uD83D\uDDE8|\uD83E\uDDD1(?:\uD83C\uDFFF\u200D[\u2695\u2696\u2708]|\uD83C\uDFFE\u200D[\u2695\u2696\u2708]|\uD83C\uDFFD\u200D[\u2695\u2696\u2708]|\uD83C\uDFFC\u200D[\u2695\u2696\u2708]|\uD83C\uDFFB\u200D[\u2695\u2696\u2708]|\u200D[\u2695\u2696\u2708])|\uD83D\uDC69(?:\uD83C\uDFFF\u200D[\u2695\u2696\u2708]|\uD83C\uDFFE\u200D[\u2695\u2696\u2708]|\uD83C\uDFFD\u200D[\u2695\u2696\u2708]|\uD83C\uDFFC\u200D[\u2695\u2696\u2708]|\uD83C\uDFFB\u200D[\u2695\u2696\u2708]|\u200D[\u2695\u2696\u2708])|\uD83D\uDE36\u200D\uD83C\uDF2B|\uD83C\uDFF3\uFE0F\u200D\u26A7|\uD83D\uDC3B\u200D\u2744|(?:(?:\uD83C[\uDFC3\uDFC4\uDFCA]|\uD83D[\uDC6E\uDC70\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6]|\uD83E[\uDD26\uDD35\uDD37-\uDD39\uDD3D\uDD3E\uDDB8\uDDB9\uDDCD-\uDDCF\uDDD4\uDDD6-\uDDDD])(?:\uD83C[\uDFFB-\uDFFF])|\uD83D\uDC6F|\uD83E[\uDD3C\uDDDE\uDDDF])\u200D[\u2640\u2642]|(?:\u26F9|\uD83C[\uDFCB\uDFCC]|\uD83D\uDD75)(?:\uFE0F|\uD83C[\uDFFB-\uDFFF])\u200D[\u2640\u2642]|\uD83C\uDFF4\u200D\u2620|(?:\uD83C[\uDFC3\uDFC4\uDFCA]|\uD83D[\uDC6E\uDC70\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6]|\uD83E[\uDD26\uDD35\uDD37-\uDD39\uDD3D\uDD3E\uDDB8\uDDB9\uDDCD-\uDDCF\uDDD4\uDDD6-\uDDDD])\u200D[\u2640\u2642]|[\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u2328\u23CF\u23ED-\u23EF\u23F1\u23F2\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB\u25FC\u2600-\u2604\u260E\u2611\u2618\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u265F\u2660\u2663\u2665\u2666\u2668\u267B\u267E\u2692\u2694-\u2697\u2699\u269B\u269C\u26A0\u26A7\u26B0\u26B1\u26C8\u26CF\u26D1\u26D3\u26E9\u26F0\u26F1\u26F4\u26F7\u26F8\u2702\u2708\u2709\u270F\u2712\u2714\u2716\u271D\u2721\u2733\u2734\u2744\u2747\u2763\u27A1\u2934\u2935\u2B05-\u2B07\u3030\u303D\u3297\u3299]|\uD83C[\uDD70\uDD71\uDD7E\uDD7F\uDE02\uDE37\uDF21\uDF24-\uDF2C\uDF36\uDF7D\uDF96\uDF97\uDF99-\uDF9B\uDF9E\uDF9F\uDFCD\uDFCE\uDFD4-\uDFDF\uDFF5\uDFF7]|\uD83D[\uDC3F\uDCFD\uDD49\uDD4A\uDD6F\uDD70\uDD73\uDD76-\uDD79\uDD87\uDD8A-\uDD8D\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA\uDECB\uDECD-\uDECF\uDEE0-\uDEE5\uDEE9\uDEF0\uDEF3])\uFE0F|\uD83C\uDFF3\uFE0F\u200D\uD83C\uDF08|\uD83D\uDC69\u200D\uD83D\uDC67|\uD83D\uDC69\u200D\uD83D\uDC66|\uD83D\uDE35\u200D\uD83D\uDCAB|\uD83D\uDE2E\u200D\uD83D\uDCA8|\uD83D\uDC15\u200D\uD83E\uDDBA|\uD83E\uDDD1(?:\uD83C\uDFFF|\uD83C\uDFFE|\uD83C\uDFFD|\uD83C\uDFFC|\uD83C\uDFFB)?|\uD83D\uDC69(?:\uD83C\uDFFF|\uD83C\uDFFE|\uD83C\uDFFD|\uD83C\uDFFC|\uD83C\uDFFB)?|\uD83C\uDDFD\uD83C\uDDF0|\uD83C\uDDF6\uD83C\uDDE6|\uD83C\uDDF4\uD83C\uDDF2|\uD83D\uDC08\u200D\u2B1B|\u2764\uFE0F\u200D(?:\uD83D\uDD25|\uD83E\uDE79)|\uD83D\uDC41\uFE0F|\uD83C\uDFF3\uFE0F|\uD83C\uDDFF(?:\uD83C[\uDDE6\uDDF2\uDDFC])|\uD83C\uDDFE(?:\uD83C[\uDDEA\uDDF9])|\uD83C\uDDFC(?:\uD83C[\uDDEB\uDDF8])|\uD83C\uDDFB(?:\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDEE\uDDF3\uDDFA])|\uD83C\uDDFA(?:\uD83C[\uDDE6\uDDEC\uDDF2\uDDF3\uDDF8\uDDFE\uDDFF])|\uD83C\uDDF9(?:\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDED\uDDEF-\uDDF4\uDDF7\uDDF9\uDDFB\uDDFC\uDDFF])|\uD83C\uDDF8(?:\uD83C[\uDDE6-\uDDEA\uDDEC-\uDDF4\uDDF7-\uDDF9\uDDFB\uDDFD-\uDDFF])|\uD83C\uDDF7(?:\uD83C[\uDDEA\uDDF4\uDDF8\uDDFA\uDDFC])|\uD83C\uDDF5(?:\uD83C[\uDDE6\uDDEA-\uDDED\uDDF0-\uDDF3\uDDF7-\uDDF9\uDDFC\uDDFE])|\uD83C\uDDF3(?:\uD83C[\uDDE6\uDDE8\uDDEA-\uDDEC\uDDEE\uDDF1\uDDF4\uDDF5\uDDF7\uDDFA\uDDFF])|\uD83C\uDDF2(?:\uD83C[\uDDE6\uDDE8-\uDDED\uDDF0-\uDDFF])|\uD83C\uDDF1(?:\uD83C[\uDDE6-\uDDE8\uDDEE\uDDF0\uDDF7-\uDDFB\uDDFE])|\uD83C\uDDF0(?:\uD83C[\uDDEA\uDDEC-\uDDEE\uDDF2\uDDF3\uDDF5\uDDF7\uDDFC\uDDFE\uDDFF])|\uD83C\uDDEF(?:\uD83C[\uDDEA\uDDF2\uDDF4\uDDF5])|\uD83C\uDDEE(?:\uD83C[\uDDE8-\uDDEA\uDDF1-\uDDF4\uDDF6-\uDDF9])|\uD83C\uDDED(?:\uD83C[\uDDF0\uDDF2\uDDF3\uDDF7\uDDF9\uDDFA])|\uD83C\uDDEC(?:\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEE\uDDF1-\uDDF3\uDDF5-\uDDFA\uDDFC\uDDFE])|\uD83C\uDDEB(?:\uD83C[\uDDEE-\uDDF0\uDDF2\uDDF4\uDDF7])|\uD83C\uDDEA(?:\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDED\uDDF7-\uDDFA])|\uD83C\uDDE9(?:\uD83C[\uDDEA\uDDEC\uDDEF\uDDF0\uDDF2\uDDF4\uDDFF])|\uD83C\uDDE8(?:\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDEE\uDDF0-\uDDF5\uDDF7\uDDFA-\uDDFF])|\uD83C\uDDE7(?:\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEF\uDDF1-\uDDF4\uDDF6-\uDDF9\uDDFB\uDDFC\uDDFE\uDDFF])|\uD83C\uDDE6(?:\uD83C[\uDDE8-\uDDEC\uDDEE\uDDF1\uDDF2\uDDF4\uDDF6-\uDDFA\uDDFC\uDDFD\uDDFF])|[#\*0-9]\uFE0F\u20E3|\u2764\uFE0F|(?:\uD83C[\uDFC3\uDFC4\uDFCA]|\uD83D[\uDC6E\uDC70\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6]|\uD83E[\uDD26\uDD35\uDD37-\uDD39\uDD3D\uDD3E\uDDB8\uDDB9\uDDCD-\uDDCF\uDDD4\uDDD6-\uDDDD])(?:\uD83C[\uDFFB-\uDFFF])|(?:\u26F9|\uD83C[\uDFCB\uDFCC]|\uD83D\uDD75)(?:\uFE0F|\uD83C[\uDFFB-\uDFFF])|\uD83C\uDFF4|(?:[\u270A\u270B]|\uD83C[\uDF85\uDFC2\uDFC7]|\uD83D[\uDC42\uDC43\uDC46-\uDC50\uDC66\uDC67\uDC6B-\uDC6D\uDC72\uDC74-\uDC76\uDC78\uDC7C\uDC83\uDC85\uDC8F\uDC91\uDCAA\uDD7A\uDD95\uDD96\uDE4C\uDE4F\uDEC0\uDECC]|\uD83E[\uDD0C\uDD0F\uDD18-\uDD1C\uDD1E\uDD1F\uDD30-\uDD34\uDD36\uDD77\uDDB5\uDDB6\uDDBB\uDDD2\uDDD3\uDDD5])(?:\uD83C[\uDFFB-\uDFFF])|(?:[\u261D\u270C\u270D]|\uD83D[\uDD74\uDD90])(?:\uFE0F|\uD83C[\uDFFB-\uDFFF])|[\u270A\u270B]|\uD83C[\uDF85\uDFC2\uDFC7]|\uD83D[\uDC08\uDC15\uDC3B\uDC42\uDC43\uDC46-\uDC50\uDC66\uDC67\uDC6B-\uDC6D\uDC72\uDC74-\uDC76\uDC78\uDC7C\uDC83\uDC85\uDC8F\uDC91\uDCAA\uDD7A\uDD95\uDD96\uDE2E\uDE35\uDE36\uDE4C\uDE4F\uDEC0\uDECC]|\uD83E[\uDD0C\uDD0F\uDD18-\uDD1C\uDD1E\uDD1F\uDD30-\uDD34\uDD36\uDD77\uDDB5\uDDB6\uDDBB\uDDD2\uDDD3\uDDD5]|\uD83C[\uDFC3\uDFC4\uDFCA]|\uD83D[\uDC6E\uDC70\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6]|\uD83E[\uDD26\uDD35\uDD37-\uDD39\uDD3D\uDD3E\uDDB8\uDDB9\uDDCD-\uDDCF\uDDD4\uDDD6-\uDDDD]|\uD83D\uDC6F|\uD83E[\uDD3C\uDDDE\uDDDF]|[\u231A\u231B\u23E9-\u23EC\u23F0\u23F3\u25FD\u25FE\u2614\u2615\u2648-\u2653\u267F\u2693\u26A1\u26AA\u26AB\u26BD\u26BE\u26C4\u26C5\u26CE\u26D4\u26EA\u26F2\u26F3\u26F5\u26FA\u26FD\u2705\u2728\u274C\u274E\u2753-\u2755\u2757\u2795-\u2797\u27B0\u27BF\u2B1B\u2B1C\u2B50\u2B55]|\uD83C[\uDC04\uDCCF\uDD8E\uDD91-\uDD9A\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF7C\uDF7E-\uDF84\uDF86-\uDF93\uDFA0-\uDFC1\uDFC5\uDFC6\uDFC8\uDFC9\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF8-\uDFFF]|\uD83D[\uDC00-\uDC07\uDC09-\uDC14\uDC16-\uDC3A\uDC3C-\uDC3E\uDC40\uDC44\uDC45\uDC51-\uDC65\uDC6A\uDC79-\uDC7B\uDC7D-\uDC80\uDC84\uDC88-\uDC8E\uDC90\uDC92-\uDCA9\uDCAB-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDDA4\uDDFB-\uDE2D\uDE2F-\uDE34\uDE37-\uDE44\uDE48-\uDE4A\uDE80-\uDEA2\uDEA4-\uDEB3\uDEB7-\uDEBF\uDEC1-\uDEC5\uDED0-\uDED2\uDED5-\uDED7\uDEEB\uDEEC\uDEF4-\uDEFC\uDFE0-\uDFEB]|\uD83E[\uDD0D\uDD0E\uDD10-\uDD17\uDD1D\uDD20-\uDD25\uDD27-\uDD2F\uDD3A\uDD3F-\uDD45\uDD47-\uDD76\uDD78\uDD7A-\uDDB4\uDDB7\uDDBA\uDDBC-\uDDCB\uDDD0\uDDE0-\uDDFF\uDE70-\uDE74\uDE78-\uDE7A\uDE80-\uDE86\uDE90-\uDEA8\uDEB0-\uDEB6\uDEC0-\uDEC2\uDED0-\uDED6]|(?:[\u231A\u231B\u23E9-\u23EC\u23F0\u23F3\u25FD\u25FE\u2614\u2615\u2648-\u2653\u267F\u2693\u26A1\u26AA\u26AB\u26BD\u26BE\u26C4\u26C5\u26CE\u26D4\u26EA\u26F2\u26F3\u26F5\u26FA\u26FD\u2705\u270A\u270B\u2728\u274C\u274E\u2753-\u2755\u2757\u2795-\u2797\u27B0\u27BF\u2B1B\u2B1C\u2B50\u2B55]|\uD83C[\uDC04\uDCCF\uDD8E\uDD91-\uDD9A\uDDE6-\uDDFF\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF7C\uDF7E-\uDF93\uDFA0-\uDFCA\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF4\uDFF8-\uDFFF]|\uD83D[\uDC00-\uDC3E\uDC40\uDC42-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDD7A\uDD95\uDD96\uDDA4\uDDFB-\uDE4F\uDE80-\uDEC5\uDECC\uDED0-\uDED2\uDED5-\uDED7\uDEEB\uDEEC\uDEF4-\uDEFC\uDFE0-\uDFEB]|\uD83E[\uDD0C-\uDD3A\uDD3C-\uDD45\uDD47-\uDD78\uDD7A-\uDDCB\uDDCD-\uDDFF\uDE70-\uDE74\uDE78-\uDE7A\uDE80-\uDE86\uDE90-\uDEA8\uDEB0-\uDEB6\uDEC0-\uDEC2\uDED0-\uDED6])|(?:[#\*0-9\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u231A\u231B\u2328\u23CF\u23E9-\u23F3\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB-\u25FE\u2600-\u2604\u260E\u2611\u2614\u2615\u2618\u261D\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u2648-\u2653\u265F\u2660\u2663\u2665\u2666\u2668\u267B\u267E\u267F\u2692-\u2697\u2699\u269B\u269C\u26A0\u26A1\u26A7\u26AA\u26AB\u26B0\u26B1\u26BD\u26BE\u26C4\u26C5\u26C8\u26CE\u26CF\u26D1\u26D3\u26D4\u26E9\u26EA\u26F0-\u26F5\u26F7-\u26FA\u26FD\u2702\u2705\u2708-\u270D\u270F\u2712\u2714\u2716\u271D\u2721\u2728\u2733\u2734\u2744\u2747\u274C\u274E\u2753-\u2755\u2757\u2763\u2764\u2795-\u2797\u27A1\u27B0\u27BF\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B50\u2B55\u3030\u303D\u3297\u3299]|\uD83C[\uDC04\uDCCF\uDD70\uDD71\uDD7E\uDD7F\uDD8E\uDD91-\uDD9A\uDDE6-\uDDFF\uDE01\uDE02\uDE1A\uDE2F\uDE32-\uDE3A\uDE50\uDE51\uDF00-\uDF21\uDF24-\uDF93\uDF96\uDF97\uDF99-\uDF9B\uDF9E-\uDFF0\uDFF3-\uDFF5\uDFF7-\uDFFF]|\uD83D[\uDC00-\uDCFD\uDCFF-\uDD3D\uDD49-\uDD4E\uDD50-\uDD67\uDD6F\uDD70\uDD73-\uDD7A\uDD87\uDD8A-\uDD8D\uDD90\uDD95\uDD96\uDDA4\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA-\uDE4F\uDE80-\uDEC5\uDECB-\uDED2\uDED5-\uDED7\uDEE0-\uDEE5\uDEE9\uDEEB\uDEEC\uDEF0\uDEF3-\uDEFC\uDFE0-\uDFEB]|\uD83E[\uDD0C-\uDD3A\uDD3C-\uDD45\uDD47-\uDD78\uDD7A-\uDDCB\uDDCD-\uDDFF\uDE70-\uDE74\uDE78-\uDE7A\uDE80-\uDE86\uDE90-\uDEA8\uDEB0-\uDEB6\uDEC0-\uDEC2\uDED0-\uDED6])\uFE0F|(?:[\u261D\u26F9\u270A-\u270D]|\uD83C[\uDF85\uDFC2-\uDFC4\uDFC7\uDFCA-\uDFCC]|\uD83D[\uDC42\uDC43\uDC46-\uDC50\uDC66-\uDC78\uDC7C\uDC81-\uDC83\uDC85-\uDC87\uDC8F\uDC91\uDCAA\uDD74\uDD75\uDD7A\uDD90\uDD95\uDD96\uDE45-\uDE47\uDE4B-\uDE4F\uDEA3\uDEB4-\uDEB6\uDEC0\uDECC]|\uD83E[\uDD0C\uDD0F\uDD18-\uDD1F\uDD26\uDD30-\uDD39\uDD3C-\uDD3E\uDD77\uDDB5\uDDB6\uDDB8\uDDB9\uDDBB\uDDCD-\uDDCF\uDDD1-\uDDDD])/g},db=i1(Fb);function p($,K={}){if(typeof $!="string"||$.length===0||(K={ambiguousIsNarrow:!0,...K},$=n1($),$.length===0))return 0;$=$.replace(db()," ");let Y=K.ambiguousIsNarrow?1:2,Q=0;for(let X of $){let x=X.codePointAt(0);if(x<=31||x>=127&&x<=159||x>=768&&x<=879)continue;switch(fb.eastAsianWidth(X)){case"F":case"W":Q+=2;break;case"A":Q+=Y;break;default:Q+=1}}return Q}var z1=10,d1=($=0)=>(K)=>`\x1B[${K+$}m`,g1=($=0)=>(K)=>`\x1B[${38+$};5;${K}m`,c1=($=0)=>(K,Y,Q)=>`\x1B[${38+$};2;${K};${Y};${Q}m`,P={modifier:{reset:[0,0],bold:[1,22],dim:[2,22],italic:[3,23],underline:[4,24],overline:[53,55],inverse:[7,27],hidden:[8,28],strikethrough:[9,29]},color:{black:[30,39],red:[31,39],green:[32,39],yellow:[33,39],blue:[34,39],magenta:[35,39],cyan:[36,39],white:[37,39],blackBright:[90,39],gray:[90,39],grey:[90,39],redBright:[91,39],greenBright:[92,39],yellowBright:[93,39],blueBright:[94,39],magentaBright:[95,39],cyanBright:[96,39],whiteBright:[97,39]},bgColor:{bgBlack:[40,49],bgRed:[41,49],bgGreen:[42,49],bgYellow:[43,49],bgBlue:[44,49],bgMagenta:[45,49],bgCyan:[46,49],bgWhite:[47,49],bgBlackBright:[100,49],bgGray:[100,49],bgGrey:[100,49],bgRedBright:[101,49],bgGreenBright:[102,49],bgYellowBright:[103,49],bgBlueBright:[104,49],bgMagentaBright:[105,49],bgCyanBright:[106,49],bgWhiteBright:[107,49]}};Object.keys(P.modifier);var gb=Object.keys(P.color),cb=Object.keys(P.bgColor);[...gb,...cb];function ub(){let $=new Map;for(let[K,Y]of Object.entries(P)){for(let[Q,X]of Object.entries(Y))P[Q]={open:`\x1B[${X[0]}m`,close:`\x1B[${X[1]}m`},Y[Q]=P[Q],$.set(X[0],X[1]);Object.defineProperty(P,K,{value:Y,enumerable:!1})}return Object.defineProperty(P,"codes",{value:$,enumerable:!1}),P.color.close="\x1B[39m",P.bgColor.close="\x1B[49m",P.color.ansi=d1(),P.color.ansi256=g1(),P.color.ansi16m=c1(),P.bgColor.ansi=d1(z1),P.bgColor.ansi256=g1(z1),P.bgColor.ansi16m=c1(z1),Object.defineProperties(P,{rgbToAnsi256:{value:(K,Y,Q)=>K===Y&&Y===Q?K<8?16:K>248?231:Math.round((K-8)/247*24)+232:16+36*Math.round(K/255*5)+6*Math.round(Y/255*5)+Math.round(Q/255*5),enumerable:!1},hexToRgb:{value:(K)=>{let Y=/[a-f\d]{6}|[a-f\d]{3}/i.exec(K.toString(16));if(!Y)return[0,0,0];let[Q]=Y;Q.length===3&&(Q=[...Q].map((x)=>x+x).join(""));let X=Number.parseInt(Q,16);return[X>>16&255,X>>8&255,X&255]},enumerable:!1},hexToAnsi256:{value:(K)=>P.rgbToAnsi256(...P.hexToRgb(K)),enumerable:!1},ansi256ToAnsi:{value:(K)=>{if(K<8)return 30+K;if(K<16)return 90+(K-8);let Y,Q,X;if(K>=232)Y=((K-232)*10+8)/255,Q=Y,X=Y;else{K-=16;let Z=K%36;Y=Math.floor(K/36)/5,Q=Math.floor(Z/6)/5,X=Z%6/5}let x=Math.max(Y,Q,X)*2;if(x===0)return 30;let b=30+(Math.round(X)<<2|Math.round(Q)<<1|Math.round(Y));return x===2&&(b+=60),b},enumerable:!1},rgbToAnsi:{value:(K,Y,Q)=>P.ansi256ToAnsi(P.rgbToAnsi256(K,Y,Q)),enumerable:!1},hexToAnsi:{value:(K)=>P.ansi256ToAnsi(P.hexToAnsi256(K)),enumerable:!1}}),P}var pb=ub(),s=new Set(["\x1B","\x9B"]),rb=39,H1="\x07",t1="[",ob="]",s1="m",M1=`${ob}8;;`,u1=($)=>`${s.values().next().value}${t1}${$}${s1}`,p1=($)=>`${s.values().next().value}${M1}${$}${H1}`,hb=($)=>$.split(" ").map((K)=>p(K)),G1=($,K,Y)=>{let Q=[...K],X=!1,x=!1,b=p(n1($[$.length-1]));for(let[Z,G]of Q.entries()){let z=p(G);if(b+z<=Y?$[$.length-1]+=G:($.push(G),b=0),s.has(G)&&(X=!0,x=Q.slice(Z+1).join("").startsWith(M1)),X){x?G===H1&&(X=!1,x=!1):G===s1&&(X=!1);continue}b+=z,b===Y&&Z0&&$.length>1&&($[$.length-2]+=$.pop())},Db=($)=>{let K=$.split(" "),Y=K.length;for(;Y>0&&!(p(K[Y-1])>0);)Y--;return Y===K.length?$:K.slice(0,Y).join(" ")+K.slice(Y).join("")},lb=($,K,Y={})=>{if(Y.trim!==!1&&$.trim()==="")return"";let Q="",X,x,b=hb($),Z=[""];for(let[z,J]of $.split(" ").entries()){Y.trim!==!1&&(Z[Z.length-1]=Z[Z.length-1].trimStart());let B=p(Z[Z.length-1]);if(z!==0&&(B>=K&&(Y.wordWrap===!1||Y.trim===!1)&&(Z.push(""),B=0),(B>0||Y.trim===!1)&&(Z[Z.length-1]+=" ",B++)),Y.hard&&b[z]>K){let W=K-B,V=1+Math.floor((b[z]-W-1)/K);Math.floor((b[z]-1)/K)K&&B>0&&b[z]>0){if(Y.wordWrap===!1&&BK&&Y.wordWrap===!1){G1(Z,J,K);continue}Z[Z.length-1]+=J}Y.trim!==!1&&(Z=Z.map((z)=>Db(z)));let G=[...Z.join(` +`)];for(let[z,J]of G.entries()){if(Q+=J,s.has(J)){let{groups:W}=new RegExp(`(?:\\${t1}(?\\d+)m|\\${M1}(?.*)${H1})`).exec(G.slice(z).join(""))||{groups:{}};if(W.code!==void 0){let V=Number.parseFloat(W.code);X=V===rb?void 0:V}else W.uri!==void 0&&(x=W.uri.length===0?void 0:W.uri)}let B=pb.codes.get(Number(X));G[z+1]===` +`?(x&&(Q+=p1("")),X&&B&&(Q+=u1(B))):J===` +`&&(X&&B&&(Q+=u1(X)),x&&(Q+=p1(x)))}return Q};function r1($,K,Y){return String($).normalize().replace(/\r\n/g,` `).split(` -`).map((K)=>Ub(K,$,B)).join(` -`)}var Cb=["up","down","left","right","space","enter","cancel"],r={actions:new Set(Cb),aliases:new Map([["k","up"],["j","down"],["h","left"],["l","right"],["\x03","cancel"],["escape","cancel"]])};function Q1(x,$){if(typeof x=="string")return r.aliases.get(x)===$;for(let B of x)if(B!==void 0&&Q1(B,$))return!0;return!1}function jb(x,$){if(x===$)return;let B=x.split(` -`),K=$.split(` -`),S=[];for(let Y=0;Y{let Z=String(b);if(Q1([Z,X,J],"cancel")){K&&$.write(T.cursor.show),process.exit(0);return}if(!B)return;j.moveCursor($,X==="return"?0:-1,X==="return"?-1:0,()=>{j.clearLine($,1,()=>{x.once("keypress",Y)})})};return K&&$.write(T.cursor.hide),x.once("keypress",Y),()=>{x.off("keypress",Y),K&&$.write(T.cursor.show),x.isTTY&&!kb&&x.setRawMode(!1),S.terminal=!1,S.close()}}var yb=Object.defineProperty,Eb=(x,$,B)=>($ in x)?yb(x,$,{enumerable:!0,configurable:!0,writable:!0,value:B}):x[$]=B,_=(x,$,B)=>(Eb(x,typeof $!="symbol"?$+"":$,B),B);class a{constructor(x,$=!0){_(this,"input"),_(this,"output"),_(this,"_abortSignal"),_(this,"rl"),_(this,"opts"),_(this,"_render"),_(this,"_track",!1),_(this,"_prevFrame",""),_(this,"_subscribers",new Map),_(this,"_cursor",0),_(this,"state","initial"),_(this,"error",""),_(this,"value");let{input:B=E1,output:K=w1,render:S,signal:Y,...b}=x;this.opts=b,this.onKeypress=this.onKeypress.bind(this),this.close=this.close.bind(this),this.render=this.render.bind(this),this._render=S.bind(this),this._track=$,this._abortSignal=Y,this.input=B,this.output=K}unsubscribe(){this._subscribers.clear()}setSubscriber(x,$){let B=this._subscribers.get(x)??[];B.push($),this._subscribers.set(x,B)}on(x,$){this.setSubscriber(x,{cb:$})}once(x,$){this.setSubscriber(x,{cb:$,once:!0})}emit(x,...$){let B=this._subscribers.get(x)??[],K=[];for(let S of B)S.cb(...$),S.once&&K.push(()=>B.splice(B.indexOf(S),1));for(let S of K)S()}prompt(){return new Promise((x,$)=>{if(this._abortSignal){if(this._abortSignal.aborted)return this.state="cancel",this.close(),x(K1);this._abortSignal.addEventListener("abort",()=>{this.state="cancel",this.close()},{once:!0})}let B=new Wb(0);B._write=(K,S,Y)=>{this._track&&(this.value=this.rl?.line.replace(/\t/g,""),this._cursor=this.rl?.cursor??0,this.emit("value",this.value)),Y()},this.input.pipe(B),this.rl=m1.createInterface({input:this.input,output:B,tabSize:2,prompt:"",escapeCodeTimeout:50}),m1.emitKeypressEvents(this.input,this.rl),this.rl.prompt(),this.opts.initialValue!==void 0&&this._track&&this.rl.write(this.opts.initialValue),this.input.on("keypress",this.onKeypress),c(this.input,!0),this.output.on("resize",this.render),this.render(),this.once("submit",()=>{this.output.write(T.cursor.show),this.output.off("resize",this.render),c(this.input,!1),x(this.value)}),this.once("cancel",()=>{this.output.write(T.cursor.show),this.output.off("resize",this.render),c(this.input,!1),x(K1)})})}onKeypress(x,$){if(this.state==="error"&&(this.state="active"),$?.name&&(!this._track&&r.aliases.has($.name)&&this.emit("cursor",r.aliases.get($.name)),r.actions.has($.name)&&this.emit("cursor",$.name)),x&&(x.toLowerCase()==="y"||x.toLowerCase()==="n")&&this.emit("confirm",x.toLowerCase()==="y"),x==="\t"&&this.opts.placeholder&&(this.value||(this.rl?.write(this.opts.placeholder),this.emit("value",this.opts.placeholder))),x&&this.emit("key",x.toLowerCase()),$?.name==="return"){if(this.opts.validate){let B=this.opts.validate(this.value);B&&(this.error=B instanceof Error?B.message:B,this.state="error",this.rl?.write(this.value))}this.state!=="error"&&(this.state="submit")}Q1([x,$?.name,$?.sequence],"cancel")&&(this.state="cancel"),(this.state==="submit"||this.state==="cancel")&&this.emit("finalize"),this.render(),(this.state==="submit"||this.state==="cancel")&&this.close()}close(){this.input.unpipe(),this.input.removeListener("keypress",this.onKeypress),this.output.write(` -`),c(this.input,!1),this.rl?.close(),this.rl=void 0,this.emit(`${this.state}`,this.value),this.unsubscribe()}restoreCursor(){let x=k1(this._prevFrame,process.stdout.columns,{hard:!0}).split(` -`).length-1;this.output.write(T.cursor.move(-999,x*-1))}render(){let x=k1(this._render(this)??"",process.stdout.columns,{hard:!0});if(x!==this._prevFrame){if(this.state==="initial")this.output.write(T.cursor.hide);else{let $=jb(this._prevFrame,x);if(this.restoreCursor(),$&&$?.length===1){let B=$[0];this.output.write(T.cursor.move(0,B)),this.output.write(T.erase.lines(1));let K=x.split(` -`);this.output.write(K[B]),this._prevFrame=x,this.output.write(T.cursor.move(0,K.length-B-1));return}if($&&$?.length>1){let B=$[0];this.output.write(T.cursor.move(0,B)),this.output.write(T.erase.down());let K=x.split(` -`).slice(B);this.output.write(K.join(` -`)),this._prevFrame=x;return}this.output.write(T.erase.down())}this.output.write(x),this.state==="initial"&&(this.state="active"),this._prevFrame=x}}}class X1 extends a{get cursor(){return this.value?0:1}get _value(){return this.cursor===0}constructor(x){super(x,!1),this.value=!!x.initialValue,this.on("value",()=>{this.value=this._value}),this.on("confirm",($)=>{this.output.write(T.cursor.move(0,-1)),this.value=$,this.state="submit",this.close()}),this.on("cursor",()=>{this.value=!this.value})}}var wb=Object.defineProperty,fb=(x,$,B)=>($ in x)?wb(x,$,{enumerable:!0,configurable:!0,writable:!0,value:B}):x[$]=B,y1=(x,$,B)=>(fb(x,typeof $!="symbol"?$+"":$,B),B);class J1 extends a{constructor(x){super(x,!1),y1(this,"options"),y1(this,"cursor",0),this.options=x.options,this.cursor=this.options.findIndex(({value:$})=>$===x.initialValue),this.cursor===-1&&(this.cursor=0),this.changeValue(),this.on("cursor",($)=>{switch($){case"left":case"up":this.cursor=this.cursor===0?this.options.length-1:this.cursor-1;break;case"down":case"right":this.cursor=this.cursor===this.options.length-1?0:this.cursor+1;break}this.changeValue()})}get _value(){return this.options[this.cursor]}changeValue(){this.value=this._value.value}}class Z1 extends a{get valueWithCursor(){if(this.state==="submit")return this.value;if(this.cursor>=this.value.length)return`${this.value}\u2588`;let x=this.value.slice(0,this.cursor),[$,...B]=this.value.slice(this.cursor);return`${x}${f1.default.inverse($)}${B.join("")}`}get cursor(){return this._cursor}constructor(x){super(x),this.on("finalize",()=>{this.value||(this.value=x.defaultValue)})}}var Q=h(x1(),1),n=h(e(),1);import C from"process";function vb(){return C.platform!=="win32"?C.env.TERM!=="linux":!!C.env.CI||!!C.env.WT_SESSION||!!C.env.TERMINUS_SUBLIME||C.env.ConEmuTask==="{cmd::Cmder}"||C.env.TERM_PROGRAM==="Terminus-Sublime"||C.env.TERM_PROGRAM==="vscode"||C.env.TERM==="xterm-256color"||C.env.TERM==="alacritty"||C.env.TERMINAL_EMULATOR==="JetBrains-JediTerm"}var z1=vb(),I=(x,$)=>z1?x:$,db=I("\u25C6","*"),c1=I("\u25A0","x"),r1=I("\u25B2","x"),u=I("\u25C7","o"),gb=I("\u250C","T"),W=I("\u2502","|"),w=I("\u2514","\u2014"),W1=I("\u25CF",">"),G1=I("\u25CB"," "),fx=I("\u25FB","[\u2022]"),vx=I("\u25FC","[+]"),dx=I("\u25FB","[ ]"),gx=I("\u25AA","\u2022"),F1=I("\u2500","-"),pb=I("\u256E","+"),hb=I("\u251C","+"),lb=I("\u256F","+"),Fb=I("\u25CF","\u2022"),cb=I("\u25C6","*"),rb=I("\u25B2","!"),ob=I("\u25A0","x"),I1=(x)=>{switch(x){case"initial":case"active":return Q.default.cyan(db);case"cancel":return Q.default.red(c1);case"error":return Q.default.yellow(r1);case"submit":return Q.default.green(u)}},ab=(x)=>{let{cursor:$,options:B,style:K}=x,S=x.maxItems??Number.POSITIVE_INFINITY,Y=Math.max(process.stdout.rows-4,0),b=Math.min(Y,Math.max(S,5)),X=0;$>=X+b-3?X=Math.max(Math.min($-b+3,B.length-b),0):$0,Z=b{let N=q===0&&J,V=q===H.length-1&&Z;return N||V?Q.default.dim("..."):K(z,q+X===$)})},d=(x)=>new Z1({validate:x.validate,placeholder:x.placeholder,defaultValue:x.defaultValue,initialValue:x.initialValue,render(){let $=`${Q.default.gray(W)} -${I1(this.state)} ${x.message} -`,B=x.placeholder?Q.default.inverse(x.placeholder[0])+Q.default.dim(x.placeholder.slice(1)):Q.default.inverse(Q.default.hidden("_")),K=this.value?this.valueWithCursor:B;switch(this.state){case"error":return`${$.trim()} -${Q.default.yellow(W)} ${K} -${Q.default.yellow(w)} ${Q.default.yellow(this.error)} -`;case"submit":return`${$}${Q.default.gray(W)} ${Q.default.dim(this.value||x.placeholder)}`;case"cancel":return`${$}${Q.default.gray(W)} ${Q.default.strikethrough(Q.default.dim(this.value??""))}${this.value?.trim()?` -${Q.default.gray(W)}`:""}`;default:return`${$}${Q.default.cyan(W)} ${K} -${Q.default.cyan(w)} -`}}}).prompt();var k=(x)=>{let $=x.active??"Yes",B=x.inactive??"No";return new X1({active:$,inactive:B,initialValue:x.initialValue??!0,render(){let K=`${Q.default.gray(W)} -${I1(this.state)} ${x.message} -`,S=this.value?$:B;switch(this.state){case"submit":return`${K}${Q.default.gray(W)} ${Q.default.dim(S)}`;case"cancel":return`${K}${Q.default.gray(W)} ${Q.default.strikethrough(Q.default.dim(S))} -${Q.default.gray(W)}`;default:return`${K}${Q.default.cyan(W)} ${this.value?`${Q.default.green(W1)} ${$}`:`${Q.default.dim(G1)} ${Q.default.dim($)}`} ${Q.default.dim("/")} ${this.value?`${Q.default.dim(G1)} ${Q.default.dim(B)}`:`${Q.default.green(W1)} ${B}`} -${Q.default.cyan(w)} -`}}}).prompt()},g=(x)=>{let $=(B,K)=>{let S=B.label??String(B.value);switch(K){case"selected":return`${Q.default.dim(S)}`;case"active":return`${Q.default.green(W1)} ${S} ${B.hint?Q.default.dim(`(${B.hint})`):""}`;case"cancelled":return`${Q.default.strikethrough(Q.default.dim(S))}`;default:return`${Q.default.dim(G1)} ${Q.default.dim(S)}`}};return new J1({options:x.options,initialValue:x.initialValue,render(){let B=`${Q.default.gray(W)} -${I1(this.state)} ${x.message} -`;switch(this.state){case"submit":return`${B}${Q.default.gray(W)} ${$(this.options[this.cursor],"selected")}`;case"cancel":return`${B}${Q.default.gray(W)} ${$(this.options[this.cursor],"cancelled")} -${Q.default.gray(W)}`;default:return`${B}${Q.default.cyan(W)} ${ab({cursor:this.cursor,options:this.options,maxItems:x.maxItems,style:(K,S)=>$(K,S?"active":"inactive")}).join(` -${Q.default.cyan(W)} `)} -${Q.default.cyan(w)} -`}}}).prompt()};var f=(x="",$="")=>{let B=` -${x} +`).map((Q)=>lb(Q,K,Y)).join(` +`)}var nb=["up","down","left","right","space","enter","cancel"],t={actions:new Set(nb),aliases:new Map([["k","up"],["j","down"],["h","left"],["l","right"],["\x03","cancel"],["escape","cancel"]])};function O1($,K){if(typeof $=="string")return t.aliases.get($)===K;for(let Y of $)if(Y!==void 0&&O1(Y,K))return!0;return!1}function ib($,K){if($===K)return;let Y=$.split(` +`),Q=K.split(` +`),X=[];for(let x=0;x{let z=String(b);if(O1([z,Z,G],"cancel")){Q&&K.write(_.cursor.show),process.exit(0);return}if(!Y)return;m.moveCursor(K,Z==="return"?0:-1,Z==="return"?-1:0,()=>{m.clearLine(K,1,()=>{$.once("keypress",x)})})};return Q&&K.write(_.cursor.hide),$.once("keypress",x),()=>{$.off("keypress",x),Q&&K.write(_.cursor.show),$.isTTY&&!ab&&$.setRawMode(!1),X.terminal=!1,X.close()}}var tb=Object.defineProperty,sb=($,K,Y)=>(K in $)?tb($,K,{enumerable:!0,configurable:!0,writable:!0,value:Y}):$[K]=Y,R=($,K,Y)=>(sb($,typeof K!="symbol"?K+"":K,Y),Y);class e{constructor($,K=!0){R(this,"input"),R(this,"output"),R(this,"_abortSignal"),R(this,"rl"),R(this,"opts"),R(this,"_render"),R(this,"_track",!1),R(this,"_prevFrame",""),R(this,"_subscribers",new Map),R(this,"_cursor",0),R(this,"state","initial"),R(this,"error",""),R(this,"value");let{input:Y=h1,output:Q=D1,render:X,signal:x,...b}=$;this.opts=b,this.onKeypress=this.onKeypress.bind(this),this.close=this.close.bind(this),this.render=this.render.bind(this),this._render=X.bind(this),this._track=K,this._abortSignal=x,this.input=Y,this.output=Q}unsubscribe(){this._subscribers.clear()}setSubscriber($,K){let Y=this._subscribers.get($)??[];Y.push(K),this._subscribers.set($,Y)}on($,K){this.setSubscriber($,{cb:K})}once($,K){this.setSubscriber($,{cb:K,once:!0})}emit($,...K){let Y=this._subscribers.get($)??[],Q=[];for(let X of Y)X.cb(...K),X.once&&Q.push(()=>Y.splice(Y.indexOf(X),1));for(let X of Q)X()}prompt(){return new Promise(($,K)=>{if(this._abortSignal){if(this._abortSignal.aborted)return this.state="cancel",this.close(),$(V1);this._abortSignal.addEventListener("abort",()=>{this.state="cancel",this.close()},{once:!0})}let Y=new Cb(0);Y._write=(Q,X,x)=>{this._track&&(this.value=this.rl?.line.replace(/\t/g,""),this._cursor=this.rl?.cursor??0,this.emit("value",this.value)),x()},this.input.pipe(Y),this.rl=F1.createInterface({input:this.input,output:Y,tabSize:2,prompt:"",escapeCodeTimeout:50}),F1.emitKeypressEvents(this.input,this.rl),this.rl.prompt(),this.opts.initialValue!==void 0&&this._track&&this.rl.write(this.opts.initialValue),this.input.on("keypress",this.onKeypress),a(this.input,!0),this.output.on("resize",this.render),this.render(),this.once("submit",()=>{this.output.write(_.cursor.show),this.output.off("resize",this.render),a(this.input,!1),$(this.value)}),this.once("cancel",()=>{this.output.write(_.cursor.show),this.output.off("resize",this.render),a(this.input,!1),$(V1)})})}onKeypress($,K){if(this.state==="error"&&(this.state="active"),K?.name&&(!this._track&&t.aliases.has(K.name)&&this.emit("cursor",t.aliases.get(K.name)),t.actions.has(K.name)&&this.emit("cursor",K.name)),$&&($.toLowerCase()==="y"||$.toLowerCase()==="n")&&this.emit("confirm",$.toLowerCase()==="y"),$==="\t"&&this.opts.placeholder&&(this.value||(this.rl?.write(this.opts.placeholder),this.emit("value",this.opts.placeholder))),$&&this.emit("key",$.toLowerCase()),K?.name==="return"){if(this.opts.validate){let Y=this.opts.validate(this.value);Y&&(this.error=Y instanceof Error?Y.message:Y,this.state="error",this.rl?.write(this.value))}this.state!=="error"&&(this.state="submit")}O1([$,K?.name,K?.sequence],"cancel")&&(this.state="cancel"),(this.state==="submit"||this.state==="cancel")&&this.emit("finalize"),this.render(),(this.state==="submit"||this.state==="cancel")&&this.close()}close(){this.input.unpipe(),this.input.removeListener("keypress",this.onKeypress),this.output.write(` +`),a(this.input,!1),this.rl?.close(),this.rl=void 0,this.emit(`${this.state}`,this.value),this.unsubscribe()}restoreCursor(){let $=r1(this._prevFrame,process.stdout.columns,{hard:!0}).split(` +`).length-1;this.output.write(_.cursor.move(-999,$*-1))}render(){let $=r1(this._render(this)??"",process.stdout.columns,{hard:!0});if($!==this._prevFrame){if(this.state==="initial")this.output.write(_.cursor.hide);else{let K=ib(this._prevFrame,$);if(this.restoreCursor(),K&&K?.length===1){let Y=K[0];this.output.write(_.cursor.move(0,Y)),this.output.write(_.erase.lines(1));let Q=$.split(` +`);this.output.write(Q[Y]),this._prevFrame=$,this.output.write(_.cursor.move(0,Q.length-Y-1));return}if(K&&K?.length>1){let Y=K[0];this.output.write(_.cursor.move(0,Y)),this.output.write(_.erase.down());let Q=$.split(` +`).slice(Y);this.output.write(Q.join(` +`)),this._prevFrame=$;return}this.output.write(_.erase.down())}this.output.write($),this.state==="initial"&&(this.state="active"),this._prevFrame=$}}}class A1 extends e{get cursor(){return this.value?0:1}get _value(){return this.cursor===0}constructor($){super($,!1),this.value=!!$.initialValue,this.on("value",()=>{this.value=this._value}),this.on("confirm",(K)=>{this.output.write(_.cursor.move(0,-1)),this.value=K,this.state="submit",this.close()}),this.on("cursor",()=>{this.value=!this.value})}}var eb=Object.defineProperty,b9=($,K,Y)=>(K in $)?eb($,K,{enumerable:!0,configurable:!0,writable:!0,value:Y}):$[K]=Y,o1=($,K,Y)=>(b9($,typeof K!="symbol"?K+"":K,Y),Y);class T1 extends e{constructor($){super($,!1),o1(this,"options"),o1(this,"cursor",0),this.options=$.options,this.cursor=this.options.findIndex(({value:K})=>K===$.initialValue),this.cursor===-1&&(this.cursor=0),this.changeValue(),this.on("cursor",(K)=>{switch(K){case"left":case"up":this.cursor=this.cursor===0?this.options.length-1:this.cursor-1;break;case"down":case"right":this.cursor=this.cursor===this.options.length-1?0:this.cursor+1;break}this.changeValue()})}get _value(){return this.options[this.cursor]}changeValue(){this.value=this._value.value}}class P1 extends e{get valueWithCursor(){if(this.state==="submit")return this.value;if(this.cursor>=this.value.length)return`${this.value}\u2588`;let $=this.value.slice(0,this.cursor),[K,...Y]=this.value.slice(this.cursor);return`${$}${l1.default.inverse(K)}${Y.join("")}`}get cursor(){return this._cursor}constructor($){super($),this.on("finalize",()=>{this.value||(this.value=$.defaultValue)})}}var q=l(W1(),1),b1=l(B1(),1);import y from"process";function $9(){return y.platform!=="win32"?y.env.TERM!=="linux":!!y.env.CI||!!y.env.WT_SESSION||!!y.env.TERMINUS_SUBLIME||y.env.ConEmuTask==="{cmd::Cmder}"||y.env.TERM_PROGRAM==="Terminus-Sublime"||y.env.TERM_PROGRAM==="vscode"||y.env.TERM==="xterm-256color"||y.env.TERM==="alacritty"||y.env.TERMINAL_EMULATOR==="JetBrains-JediTerm"}var I1=$9(),S=($,K)=>I1?$:K,K9=S("\u25C6","*"),$b=S("\u25A0","x"),Kb=S("\u25B2","x"),$1=S("\u25C7","o"),Y9=S("\u250C","T"),T=S("\u2502","|"),g=S("\u2514","\u2014"),L1=S("\u25CF",">"),U1=S("\u25CB"," "),X4=S("\u25FB","[\u2022]"),x4=S("\u25FC","[+]"),Z4=S("\u25FB","[ ]"),q4=S("\u25AA","\u2022"),bb=S("\u2500","-"),Q9=S("\u256E","+"),X9=S("\u251C","+"),x9=S("\u256F","+"),Z9=S("\u25CF","\u2022"),q9=S("\u25C6","*"),B9=S("\u25B2","!"),J9=S("\u25A0","x"),_1=($)=>{switch($){case"initial":case"active":return q.default.cyan(K9);case"cancel":return q.default.red($b);case"error":return q.default.yellow(Kb);case"submit":return q.default.green($1)}},W9=($)=>{let{cursor:K,options:Y,style:Q}=$,X=$.maxItems??Number.POSITIVE_INFINITY,x=Math.max(process.stdout.rows-4,0),b=Math.min(x,Math.max(X,5)),Z=0;K>=Z+b-3?Z=Math.max(Math.min(K-b+3,Y.length-b),0):K0,z=b{let V=B===0&&G,M=B===W.length-1&&z;return V||M?q.default.dim("..."):Q(J,B+Z===K)})},c=($)=>new P1({validate:$.validate,placeholder:$.placeholder,defaultValue:$.defaultValue,initialValue:$.initialValue,render(){let K=`${q.default.gray(T)} +${_1(this.state)} ${$.message} +`,Y=$.placeholder?q.default.inverse($.placeholder[0])+q.default.dim($.placeholder.slice(1)):q.default.inverse(q.default.hidden("_")),Q=this.value?this.valueWithCursor:Y;switch(this.state){case"error":return`${K.trim()} +${q.default.yellow(T)} ${Q} +${q.default.yellow(g)} ${q.default.yellow(this.error)} +`;case"submit":return`${K}${q.default.gray(T)} ${q.default.dim(this.value||$.placeholder)}`;case"cancel":return`${K}${q.default.gray(T)} ${q.default.strikethrough(q.default.dim(this.value??""))}${this.value?.trim()?` +${q.default.gray(T)}`:""}`;default:return`${K}${q.default.cyan(T)} ${Q} +${q.default.cyan(g)} +`}}}).prompt();var w=($)=>{let K=$.active??"Yes",Y=$.inactive??"No";return new A1({active:K,inactive:Y,initialValue:$.initialValue??!0,render(){let Q=`${q.default.gray(T)} +${_1(this.state)} ${$.message} +`,X=this.value?K:Y;switch(this.state){case"submit":return`${Q}${q.default.gray(T)} ${q.default.dim(X)}`;case"cancel":return`${Q}${q.default.gray(T)} ${q.default.strikethrough(q.default.dim(X))} +${q.default.gray(T)}`;default:return`${Q}${q.default.cyan(T)} ${this.value?`${q.default.green(L1)} ${K}`:`${q.default.dim(U1)} ${q.default.dim(K)}`} ${q.default.dim("/")} ${this.value?`${q.default.dim(U1)} ${q.default.dim(Y)}`:`${q.default.green(L1)} ${Y}`} +${q.default.cyan(g)} +`}}}).prompt()},r=($)=>{let K=(Y,Q)=>{let X=Y.label??String(Y.value);switch(Q){case"selected":return`${q.default.dim(X)}`;case"active":return`${q.default.green(L1)} ${X} ${Y.hint?q.default.dim(`(${Y.hint})`):""}`;case"cancelled":return`${q.default.strikethrough(q.default.dim(X))}`;default:return`${q.default.dim(U1)} ${q.default.dim(X)}`}};return new T1({options:$.options,initialValue:$.initialValue,render(){let Y=`${q.default.gray(T)} +${_1(this.state)} ${$.message} +`;switch(this.state){case"submit":return`${Y}${q.default.gray(T)} ${K(this.options[this.cursor],"selected")}`;case"cancel":return`${Y}${q.default.gray(T)} ${K(this.options[this.cursor],"cancelled")} +${q.default.gray(T)}`;default:return`${Y}${q.default.cyan(T)} ${W9({cursor:this.cursor,options:this.options,maxItems:$.maxItems,style:(Q,X)=>K(Q,X?"active":"inactive")}).join(` +${q.default.cyan(T)} `)} +${q.default.cyan(g)} +`}}}).prompt()};var E=($="",K="")=>{let Y=` +${$} `.split(` -`),K=q1($).length,S=Math.max(B.reduce((b,X)=>{let J=q1(X);return J.length>b?J.length:b},0),K)+2,Y=B.map((b)=>`${Q.default.gray(W)} ${Q.default.dim(b)}${" ".repeat(S-q1(b).length)}${Q.default.gray(W)}`).join(` -`);process.stdout.write(`${Q.default.gray(W)} -${Q.default.green(u)} ${Q.default.reset($)} ${Q.default.gray(F1.repeat(Math.max(S-K-1,1))+pb)} -${Y} -${Q.default.gray(hb+F1.repeat(S+2)+lb)} -`)},p=(x="")=>{process.stdout.write(`${Q.default.gray(w)} ${Q.default.red(x)} +`),Q=S1(K).length,X=Math.max(Y.reduce((b,Z)=>{let G=S1(Z);return G.length>b?G.length:b},0),Q)+2,x=Y.map((b)=>`${q.default.gray(T)} ${q.default.dim(b)}${" ".repeat(X-S1(b).length)}${q.default.gray(T)}`).join(` +`);process.stdout.write(`${q.default.gray(T)} +${q.default.green($1)} ${q.default.reset(K)} ${q.default.gray(bb.repeat(Math.max(X-Q-1,1))+Q9)} +${x} +${q.default.gray(X9+bb.repeat(X+2)+x9)} +`)},f=($="")=>{process.stdout.write(`${q.default.gray(g)} ${q.default.red($)} -`)},H1=(x="")=>{process.stdout.write(`${Q.default.gray(gb)} ${x} -`)},o1=(x="")=>{process.stdout.write(`${Q.default.gray(W)} -${Q.default.gray(w)} ${x} +`)},o=($="")=>{process.stdout.write(`${q.default.gray(Y9)} ${$} +`)},h=($="")=>{process.stdout.write(`${q.default.gray(T)} +${q.default.gray(g)} ${$} -`)},m={message:(x="",{symbol:$=Q.default.gray(W)}={})=>{let B=[`${Q.default.gray(W)}`];if(x){let[K,...S]=x.split(` -`);B.push(`${$} ${K}`,...S.map((Y)=>`${Q.default.gray(W)} ${Y}`))}process.stdout.write(`${B.join(` +`)},U={message:($="",{symbol:K=q.default.gray(T)}={})=>{let Y=[`${q.default.gray(T)}`];if($){let[Q,...X]=$.split(` +`);Y.push(`${K} ${Q}`,...X.map((x)=>`${q.default.gray(T)} ${x}`))}process.stdout.write(`${Y.join(` `)} -`)},info:(x)=>{m.message(x,{symbol:Q.default.blue(Fb)})},success:(x)=>{m.message(x,{symbol:Q.default.green(cb)})},step:(x)=>{m.message(x,{symbol:Q.default.green(u)})},warn:(x)=>{m.message(x,{symbol:Q.default.yellow(rb)})},warning:(x)=>{m.warn(x)},error:(x)=>{m.message(x,{symbol:Q.default.red(ob)})}},a1=()=>{let x=z1?["\u25D2","\u25D0","\u25D3","\u25D1"]:["\u2022","o","O","0"],$=z1?80:120,B=process.env.CI==="true",K,S,Y=!1,b="",X,J=(P)=>{let R=P>1?"Something went wrong":"Canceled";Y&&M(R,P)},Z=()=>J(2),z=()=>J(1),q=()=>{process.on("uncaughtExceptionMonitor",Z),process.on("unhandledRejection",Z),process.on("SIGINT",z),process.on("SIGTERM",z),process.on("exit",J)},H=()=>{process.removeListener("uncaughtExceptionMonitor",Z),process.removeListener("unhandledRejection",Z),process.removeListener("SIGINT",z),process.removeListener("SIGTERM",z),process.removeListener("exit",J)},N=()=>{if(X===void 0)return;B&&process.stdout.write(` -`);let P=X.split(` -`);process.stdout.write(n.cursor.move(-999,P.length-1)),process.stdout.write(n.erase.down(P.length))},V=(P)=>P.replace(/\.+$/,""),U=(P="")=>{Y=!0,K=l1(),b=V(P),process.stdout.write(`${Q.default.gray(W)} -`);let R=0,O=0;q(),S=setInterval(()=>{if(B&&b===X)return;N(),X=b;let E=Q.default.magenta(x[R]),Bb=B?"...":".".repeat(Math.floor(O)).slice(0,3);process.stdout.write(`${E} ${b}${Bb}`),R=R+1{Y=!1,clearInterval(S),N();let O=R===0?Q.default.green(u):R===1?Q.default.red(c1):Q.default.red(r1);b=V(P??b),process.stdout.write(`${O} ${b} -`),H(),K()};return{start:U,stop:M,message:(P="")=>{b=V(P??b)}}},V1=async(x,$)=>{let B={},K=Object.keys(x);for(let S of K){let Y=x[S],b=await Y({results:B})?.catch((X)=>{throw X});if(typeof $?.onCancel=="function"&&y(b)){B[S]="canceled",$.onCancel({results:B});continue}B[S]=b}return B};import{readFile as t1,writeFile as i,mkdir as O1,access as D}from"fs/promises";import{execSync as D1,exec as ub}from"child_process";import{createHash as ib,randomBytes as tb}from"crypto";import{homedir as Db}from"os";import{join as A}from"path";var A1="0.1.0",sb=process.env.SEED_DIR||"/opt/seed",eb="https://raw.githubusercontent.com/seed-hypermedia/seed/main",s1=`${process.env.SEED_REPO_URL||eb}/ops/docker-compose.yml`,bx="https://notify.seed.hyper.media",xx="https://ln.seed.hyper.media",$x="https://ln.testnet.seed.hyper.media";function Bx(x=sb){return{seedDir:x,configPath:A(x,"config.json"),composePath:A(x,"docker-compose.yml"),deployLog:A(x,"deploy.log")}}function Kx(){return{run(x){return D1(x,{encoding:"utf-8",timeout:30000}).trim()},runSafe(x){try{return this.run(x)}catch{return null}},exec(x){return new Promise(($,B)=>{ub(x,{timeout:120000},(K,S,Y)=>{if(K)B(K);else $({stdout:S.toString().trim(),stderr:Y.toString().trim()})})})}}}function e1(x){switch(x){case"dev":return{testnet:!0,release_channel:"dev"};case"staging":return{testnet:!1,release_channel:"dev"};case"prod":default:return{testnet:!1,release_channel:"latest"}}}async function Sx(x){try{return await D(x.configPath),!0}catch{return!1}}async function Yx(x){let $=await t1(x.configPath,"utf-8");return JSON.parse($)}async function t(x,$){await O1($.seedDir,{recursive:!0}),await i($.configPath,JSON.stringify(x,null,2)+` -`,"utf-8")}function bb(x=10){let B=tb(x);return Array.from(B).map((K)=>"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"[K%62]).join("")}function L(x){let $=new Date().toISOString();if(!process.stdout.isTTY)console.log(`[${$}] ${x}`)}function Qx(x){let $=null,B=!1;try{let K=JSON.parse(x);for(let S of K){if(S.startsWith("SEED_LOG_LEVEL="))$=S.split("=")[1];if(S.startsWith("SEED_P2P_TESTNET_NAME=")&&S.split("=")[1])B=!0}}catch{}return{logLevel:$,testnet:B}}function Xx(x){let $=null,B=!1,K=!1;try{let S=JSON.parse(x);for(let Y of S){if(Y.startsWith("SEED_BASE_URL="))$=Y.split("=")[1];if(Y.startsWith("SEED_IS_GATEWAY=true"))B=!0;if(Y.startsWith("SEED_ENABLE_STATISTICS=true"))K=!0}}catch{}return{hostname:$,gateway:B,trafficStats:K}}function Jx(x){let $=x.split(":");return $.length>1?$[$.length-1]:"latest"}async function Zx(x){let $=Db(),B=[A($,".seed-site"),"/shm/gateway","/shm"],K=null;for(let M of B)try{await D(M),K=M;break}catch{}let S=x.runSafe("docker ps --format '{{.Names}}' 2>/dev/null | grep -q seed");if(!K&&S===null)return null;if(!K)K=A($,".seed-site");let Y=null,b=[A(K,"web","config.json"),"/shm/gateway/web/config.json",A($,".seed-site","web","config.json")];for(let M of b)try{let P=await t1(M,"utf-8"),R=JSON.parse(P);if(R.availableRegistrationSecret){Y=R.availableRegistrationSecret;break}}catch{}let X=null,J=null,Z=null,z=!1,q=!1,H=!1,N=x.runSafe("docker inspect seed-daemon --format '{{json .Config.Env}}' 2>/dev/null");if(N){let M=Qx(N);J=M.logLevel,z=M.testnet}let V=x.runSafe("docker inspect seed-web --format '{{json .Config.Env}}' 2>/dev/null");if(V){let M=Xx(V);X=M.hostname,q=M.gateway,H=M.trafficStats}let U=x.runSafe("docker inspect seed-web --format '{{.Config.Image}}' 2>/dev/null");if(U)Z=Jx(U);return{workspace:K,secret:Y,hostname:X,logLevel:J,imageTag:Z,testnet:z,gateway:q,trafficStats:H}}async function qx(x,$,B){H1(`Seed Node Migration v${A1}`),f([`Detected an existing Seed installation at: ${x.workspace}`,"","We'll import your current settings and migrate to the new deployment system.",`After migration, your node will be managed from ${$.seedDir}/ and updated via cron.`,"","Please review and confirm the detected values below."].join(` -`),"Existing installation found");let K=await V1({domain:()=>d({message:"Public hostname (including https://)",placeholder:"https://node1.seed.run",initialValue:x.hostname??"",validate:(V)=>{if(!V)return"Required";if(!V.startsWith("https://")&&!V.startsWith("http://"))return"Must start with https:// or http://"}}),email:()=>d({message:"Contact email \u2014 lets us notify you about security updates and node issues. Not shared publicly.",placeholder:"you@example.com",validate:(V)=>{if(!V)return"Required";if(!V.includes("@"))return"Must be a valid email"}}),environment:()=>g({message:"Environment",initialValue:x.testnet?"dev":"prod",options:[{value:"prod",label:"Production",hint:"stable releases, mainnet network \u2014 recommended"},{value:"staging",label:"Staging",hint:"development builds, mainnet network \u2014 for testing"},{value:"dev",label:"Development",hint:"development builds, testnet network"}]}),log_level:()=>g({message:"Log level",initialValue:x.logLevel??"info",options:[{value:"debug",label:"Debug",hint:"verbose, useful for troubleshooting"},{value:"info",label:"Info",hint:"standard operational logging"},{value:"warn",label:"Warn",hint:"only warnings and errors"},{value:"error",label:"Error",hint:"only errors"}]}),gateway:()=>k({message:"Run as public gateway?",initialValue:x.gateway}),analytics:()=>k({message:"Enable web analytics? Adds a Plausible.io dashboard to track your site's traffic.",initialValue:x.trafficStats})},{onCancel:()=>{p("Migration cancelled"),process.exit(0)}}),S=x.secret??bb();if(x.secret)m.success("Registration secret imported from existing installation.");else m.warn("No existing registration secret found. Generated a new one.");let Y=K.environment,b=e1(Y),X={domain:K.domain,email:K.email,compose_url:s1,compose_sha:"",compose_envs:{LOG_LEVEL:K.log_level},environment:Y,release_channel:b.release_channel,testnet:b.testnet,link_secret:S,analytics:K.analytics,gateway:K.gateway,last_script_run:""},J=Object.entries(X).map(([V,U])=>` ${V}: ${typeof U==="object"?JSON.stringify(U):U}`).join(` -`);f(J,"Configuration summary");let Z=await k({message:"Write config and proceed with deployment?"});if(y(Z)||!Z)p("Migration cancelled"),process.exit(0);await t(X,$),m.success(`Config written to ${$.configPath}`);let z=A(x.workspace,"web"),q=String(process.getuid()),H=String(process.getgid()),N=B.runSafe(`stat -c '%u:%g' "${z}" 2>/dev/null`);if(N&&N!==`${q}:${H}`){if(m.warn(`The web data directory (${z}) is owned by a different user (${N}). Updating ownership so the web container can write to it.`),!B.runSafe(`chown -R ${q}:${H} "${z}" 2>/dev/null`))B.runSafe(`sudo chown -R ${q}:${H} "${z}"`);m.success("File ownership updated.")}return X}async function zx(x){H1(`Seed Node Setup v${A1}`),f(["Welcome! This wizard will configure your new Seed node.","","Seed is a peer-to-peer hypermedia publishing system. This script sets up","the Docker containers, reverse proxy, and networking so your node is","reachable on the public internet.","",`Configuration will be saved to ${x.configPath}.`,"Subsequent runs of this script will deploy automatically (headless mode)."].join(` -`),"First-time setup");let $=await V1({domain:()=>d({message:"Public hostname (including https://)",placeholder:"https://node1.seed.run",validate:(J)=>{if(!J)return"Required";if(!J.startsWith("https://")&&!J.startsWith("http://"))return"Must start with https:// or http://"}}),email:()=>d({message:"Contact email \u2014 lets us notify you about security updates and node issues. Not shared publicly.",placeholder:"you@example.com",validate:(J)=>{if(!J)return"Required";if(!J.includes("@"))return"Must be a valid email"}}),environment:()=>g({message:"Environment",initialValue:"prod",options:[{value:"prod",label:"Production",hint:"stable releases, mainnet network \u2014 recommended"},{value:"staging",label:"Staging",hint:"development builds, mainnet network \u2014 for testing"},{value:"dev",label:"Development",hint:"development builds, testnet network"}]}),log_level:()=>g({message:"Log level for Seed services",initialValue:"info",options:[{value:"debug",label:"Debug",hint:"very verbose, useful for troubleshooting"},{value:"info",label:"Info",hint:"standard operational logging \u2014 recommended"},{value:"warn",label:"Warn",hint:"only warnings and errors"},{value:"error",label:"Error",hint:"only critical errors"}]}),gateway:()=>k({message:"Run as a public gateway? (serves all known public content)",initialValue:!1}),analytics:()=>k({message:"Enable web analytics? Adds a Plausible.io dashboard to track your site's traffic.",initialValue:!1})},{onCancel:()=>{p("Setup cancelled"),process.exit(0)}}),B=bb(),K=$.environment,S=e1(K),Y={domain:$.domain,email:$.email,compose_url:s1,compose_sha:"",compose_envs:{LOG_LEVEL:$.log_level},environment:K,release_channel:S.release_channel,testnet:S.testnet,link_secret:B,analytics:$.analytics,gateway:$.gateway,last_script_run:""},b=Object.entries(Y).filter(([J])=>J!=="compose_sha"&&J!=="last_script_run").map(([J,Z])=>` ${J}: ${typeof Z==="object"?JSON.stringify(Z):Z}`).join(` -`);f(b,"Configuration summary");let X=await k({message:"Write config and proceed with deployment?"});if(y(X)||!X)p("Setup cancelled"),process.exit(0);return await t(Y,x),m.success(`Config written to ${x.configPath}`),Y}function Wx(x){return x.replace(/^https?:\/\//,"").replace(/\/+$/,"")}function Gx(x){return`{$SEED_SITE_HOSTNAME} +`)},info:($)=>{U.message($,{symbol:q.default.blue(Z9)})},success:($)=>{U.message($,{symbol:q.default.green(q9)})},step:($)=>{U.message($,{symbol:q.default.green($1)})},warn:($)=>{U.message($,{symbol:q.default.yellow(B9)})},warning:($)=>{U.warn($)},error:($)=>{U.message($,{symbol:q.default.red(J9)})}},Yb=()=>{let $=I1?["\u25D2","\u25D0","\u25D3","\u25D1"]:["\u2022","o","O","0"],K=I1?80:120,Y=process.env.CI==="true",Q,X,x=!1,b="",Z,G=(H)=>{let L=H>1?"Something went wrong":"Canceled";x&&j(L,H)},z=()=>G(2),J=()=>G(1),B=()=>{process.on("uncaughtExceptionMonitor",z),process.on("unhandledRejection",z),process.on("SIGINT",J),process.on("SIGTERM",J),process.on("exit",G)},W=()=>{process.removeListener("uncaughtExceptionMonitor",z),process.removeListener("unhandledRejection",z),process.removeListener("SIGINT",J),process.removeListener("SIGTERM",J),process.removeListener("exit",G)},V=()=>{if(Z===void 0)return;Y&&process.stdout.write(` +`);let H=Z.split(` +`);process.stdout.write(b1.cursor.move(-999,H.length-1)),process.stdout.write(b1.erase.down(H.length))},M=(H)=>H.replace(/\.+$/,""),N=(H="")=>{x=!0,Q=e1(),b=M(H),process.stdout.write(`${q.default.gray(T)} +`);let L=0,O=0;B(),X=setInterval(()=>{if(Y&&b===Z)return;V(),Z=b;let d=q.default.magenta($[L]),Ib=Y?"...":".".repeat(Math.floor(O)).slice(0,3);process.stdout.write(`${d} ${b}${Ib}`),L=L+1<$.length?L+1:0,O=O<$.length?O+0.125:0},K)},j=(H="",L=0)=>{x=!1,clearInterval(X),V();let O=L===0?q.default.green($1):L===1?q.default.red($b):q.default.red(Kb);b=M(H??b),process.stdout.write(`${O} ${b} +`),W(),Q()};return{start:N,stop:j,message:(H="")=>{b=M(H??b)}}},N1=async($,K)=>{let Y={},Q=Object.keys($);for(let X of Q){let x=$[X],b=await x({results:Y})?.catch((Z)=>{throw Z});if(typeof K?.onCancel=="function"&&k(b)){Y[X]="canceled",K.onCancel({results:Y});continue}Y[X]=b}return Y};import{readFile as R1,writeFile as u,mkdir as Z1,access as F}from"fs/promises";import{execSync as Zb,exec as G9}from"child_process";import{createHash as V9,randomBytes as H9}from"crypto";import{homedir as M9}from"os";import{join as A,basename as qb,dirname as Y1}from"path";var C="0.1.0",Bb=process.env.SEED_DIR||Y1(process.argv[1]),O9="https://raw.githubusercontent.com/seed-hypermedia/seed/main",k1=process.env.SEED_DEPLOY_URL||(process.env.SEED_REPO_URL?`${process.env.SEED_REPO_URL}/ops`:`${O9}/ops`),Jb=`${k1}/docker-compose.yml`,A9="https://notify.seed.hyper.media",T9="https://ln.seed.hyper.media",P9="https://ln.testnet.seed.hyper.media";function S9($=Bb){return{seedDir:$,configPath:A($,"config.json"),composePath:A($,"docker-compose.yml"),deployLog:A($,"deploy.log")}}function I9(){return{run($){return Zb($,{encoding:"utf-8",timeout:30000}).trim()},runSafe($){try{return this.run($)}catch{return null}},exec($){return new Promise((K,Y)=>{G9($,{timeout:120000},(Q,X,x)=>{if(Q)Y(Q);else K({stdout:X.toString().trim(),stderr:x.toString().trim()})})})}}}function Wb($){switch($){case"dev":return{testnet:!0,release_channel:"dev"};case"staging":return{testnet:!1,release_channel:"dev"};case"prod":default:return{testnet:!1,release_channel:"latest"}}}async function D($){try{return await F($.configPath),!0}catch{return!1}}async function v($){let K=await R1($.configPath,"utf-8");return JSON.parse(K)}async function Q1($,K){await Z1(K.seedDir,{recursive:!0}),await u(K.configPath,JSON.stringify($,null,2)+` +`,"utf-8")}function zb($=10){let Y=H9($);return Array.from(Y).map((Q)=>"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"[Q%62]).join("")}function I($){let K=new Date().toISOString();if(!process.stdout.isTTY)console.log(`[${K}] ${$}`)}var X1=` +Run 'seed-deploy' to resume installation at any time. +`,x1="Manage your node anytime with the 'seed-deploy' command. Run 'seed-deploy --help' for options.";function L9($){if($.testnet)return"dev";if($.imageTag==="dev")return"staging";return"prod"}function U9($){let K=null,Y=!1;try{let Q=JSON.parse($);for(let X of Q){if(X.startsWith("SEED_LOG_LEVEL="))K=X.split("=")[1];if(X.startsWith("SEED_P2P_TESTNET_NAME=")&&X.split("=")[1])Y=!0}}catch{}return{logLevel:K,testnet:Y}}function _9($){let K=null,Y=!1,Q=!1;try{let X=JSON.parse($);for(let x of X){if(x.startsWith("SEED_BASE_URL="))K=x.split("=")[1];if(x.startsWith("SEED_IS_GATEWAY=true"))Y=!0;if(x.startsWith("SEED_ENABLE_STATISTICS=true"))Q=!0}}catch{}return{hostname:K,gateway:Y,trafficStats:Q}}function N9($){let K=$.split(":");return K.length>1?K[K.length-1]:"latest"}async function j9($){let K=M9(),Y=[A(K,".seed-site"),"/shm/gateway","/shm"],Q=null;for(let H of Y)try{await F(H),Q=H;break}catch{}let X=$.runSafe("docker ps --format '{{.Names}}' 2>/dev/null | grep -q seed");if(!Q&&X===null)return null;if(!Q)Q=A(K,".seed-site");let x=null,b=!1,Z=[A(Q,"web","config.json"),"/shm/gateway/web/config.json",A(K,".seed-site","web","config.json")];for(let H of Z)try{let L=await R1(H,"utf-8"),O=JSON.parse(L);if(O.availableRegistrationSecret){x=O.availableRegistrationSecret;break}if(O.registeredAccountUid||O.sourcePeerId)b=!0}catch{}let G=null,z=null,J=null,B=!1,W=!1,V=!1,M=$.runSafe("docker inspect seed-daemon --format '{{json .Config.Env}}' 2>/dev/null");if(M){let H=U9(M);z=H.logLevel,B=H.testnet}let N=$.runSafe("docker inspect seed-web --format '{{json .Config.Env}}' 2>/dev/null");if(N){let H=_9(N);G=H.hostname,W=H.gateway,V=H.trafficStats}let j=$.runSafe("docker inspect seed-web --format '{{.Config.Image}}' 2>/dev/null");if(j)J=N9(j);return{workspace:Q,secret:x,secretConsumed:b,hostname:G,logLevel:z,imageTag:J,testnet:B,gateway:W,trafficStats:V}}async function Gb($,K,Y){o(`Seed Node Migration v${C}`),E([`Detected an existing Seed installation at: ${$.workspace}`,"","We'll import your current settings and migrate to the new deployment system.",`After migration, your node will be managed from ${K.seedDir}/ and updated via cron.`,"","Please review and confirm the detected values below."].join(` +`),"Existing installation found");let Q=await N1({domain:()=>c({message:"Public hostname (including https://)",placeholder:$.hostname||"https://node1.seed.run",validate:(M)=>{if(!M)return"Required";if(!M.startsWith("https://")&&!M.startsWith("http://"))return"Must start with https:// or http://"}}),environment:()=>r({message:"Environment",initialValue:L9($),options:[{value:"prod",label:"Production",hint:"stable releases, mainnet network \u2014 recommended"},{value:"staging",label:"Staging",hint:"development builds, mainnet network \u2014 for testing"},{value:"dev",label:"Development",hint:"development builds, testnet network"}]}),log_level:()=>r({message:"Log level",initialValue:$.logLevel??"info",options:[{value:"debug",label:"Debug",hint:"verbose, useful for troubleshooting"},{value:"info",label:"Info",hint:"standard operational logging"},{value:"warn",label:"Warn",hint:"only warnings and errors"},{value:"error",label:"Error",hint:"only errors"}]}),gateway:()=>w({message:"Run as public gateway?",initialValue:$.gateway}),analytics:()=>w({message:"Enable web analytics? Adds a Plausible.io dashboard to track your site's traffic.",initialValue:$.trafficStats}),email:()=>c({message:"Contact email (optional) \u2014 lets us notify you about security updates. Not shared publicly.",placeholder:"you@example.com",validate:(M)=>{if(M&&!M.includes("@"))return"Must be a valid email"}})},{onCancel:()=>{f("Migration cancelled."),console.log(X1),process.exit(0)}}),X=$.secret??zb();if($.secret)U.success("Registration secret imported from existing installation.");else if($.secretConsumed)U.info("Node is already registered (secret was consumed). Generated a new secret for future registrations.");else U.warn("No existing registration secret found. Generated a new one.");let x=Q.environment,b=Wb(x),Z={domain:Q.domain,email:Q.email||"",compose_url:Jb,compose_sha:"",compose_envs:{LOG_LEVEL:Q.log_level},environment:x,release_channel:b.release_channel,testnet:b.testnet,link_secret:X,analytics:Q.analytics,gateway:Q.gateway,last_script_run:""},G=Object.entries(Z).map(([M,N])=>` ${M}: ${typeof N==="object"?JSON.stringify(N):N}`).join(` +`);E(G,"Configuration summary");let z=await w({message:"Write config and proceed with deployment?"});if(k(z)||!z)f("Migration cancelled."),console.log(X1),process.exit(0);await Q1(Z,K),U.success(`Config written to ${K.configPath}`);let J=A($.workspace,"web"),B=String(process.getuid()),W=String(process.getgid()),V=Y.runSafe(`stat -c '%u:%g' "${J}" 2>/dev/null`);if(V&&V!==`${B}:${W}`){if(U.warn(`The web data directory (${J}) is owned by a different user (${V}). Updating ownership so the web container can write to it.`),!Y.runSafe(`chown -R ${B}:${W} "${J}" 2>/dev/null`))Y.runSafe(`sudo chown -R ${B}:${W} "${J}"`);U.success("File ownership updated.")}return Z}async function Qb($,K){let Y=!!K;if(o(Y?`Seed Node Reconfiguration v${C}`:`Seed Node Setup v${C}`),!Y)E(["Welcome! This wizard will configure your new Seed node.","","Seed is a peer-to-peer hypermedia publishing system. This script sets up","the Docker containers, reverse proxy, and networking so your node is","reachable on the public internet.","",`Configuration will be saved to ${$.configPath}.`,"Subsequent runs of this script will deploy automatically (headless mode)."].join(` +`),"First-time setup");else E(["Editing your current configuration. Press Tab to keep existing values, or type to change them.","",`Configuration: ${$.configPath}`].join(` +`),"Reconfiguration");let Q=await N1({domain:()=>c({message:"Public hostname (including https://)",placeholder:K?.domain||"https://node1.seed.run",validate:(W)=>{if(!W)return"Required";if(!W.startsWith("https://")&&!W.startsWith("http://"))return"Must start with https:// or http://"}}),environment:()=>r({message:"Environment",initialValue:K?.environment??"prod",options:[{value:"prod",label:"Production",hint:"stable releases, mainnet network \u2014 recommended"},{value:"staging",label:"Staging",hint:"development builds, mainnet network \u2014 for testing"},{value:"dev",label:"Development",hint:"development builds, testnet network"}]}),log_level:()=>r({message:"Log level for Seed services",initialValue:K?.compose_envs?.LOG_LEVEL??"info",options:[{value:"debug",label:"Debug",hint:"very verbose, useful for troubleshooting"},{value:"info",label:"Info",hint:"standard operational logging \u2014 recommended"},{value:"warn",label:"Warn",hint:"only warnings and errors"},{value:"error",label:"Error",hint:"only critical errors"}]}),gateway:()=>w({message:"Run as a public gateway? (serves all known public content)",initialValue:K?.gateway??!1}),analytics:()=>w({message:"Enable web analytics? Adds a Plausible.io dashboard to track your site's traffic.",initialValue:K?.analytics??!1}),email:()=>c({message:"Contact email (optional) \u2014 lets us notify you about security updates. Not shared publicly.",placeholder:K?.email||"you@example.com",validate:(W)=>{if(W&&!W.includes("@"))return"Must be a valid email"}})},{onCancel:()=>{f(Y?"Reconfiguration cancelled.":"Setup cancelled."),console.log(X1),process.exit(0)}}),X=K?.link_secret??zb(),x=Q.environment,b=Wb(x),Z={domain:Q.domain,email:Q.email||"",compose_url:Jb,compose_sha:K?.compose_sha??"",compose_envs:{LOG_LEVEL:Q.log_level},environment:x,release_channel:b.release_channel,testnet:b.testnet,link_secret:X,analytics:Q.analytics,gateway:Q.gateway,last_script_run:K?.last_script_run??""},G=[["domain",Z.domain],["email",Z.email],["environment",Z.environment],["log_level",Z.compose_envs.LOG_LEVEL],["gateway",String(Z.gateway)],["analytics",String(Z.analytics)]],z=K?{domain:K.domain,email:K.email,environment:K.environment,log_level:K.compose_envs?.LOG_LEVEL??"info",gateway:String(K.gateway),analytics:String(K.analytics)}:void 0,J=G.map(([W,V])=>{if(z&&String(V)!==String(z[W]??""))return` \u270E ${W}: ${V}`;return` ${W}: ${V}`}).join(` +`);E(J,"Configuration summary");let B=await w({message:Y?"Save changes and redeploy?":"Write config and proceed with deployment?"});if(k(B)||!B)f(Y?"Reconfiguration cancelled.":"Setup cancelled."),console.log(X1),process.exit(0);return await Q1(Z,$),U.success(`Config written to ${$.configPath}`),Z}function Vb($){return $.replace(/^https?:\/\//,"").replace(/\/+$/,"")}function R9($){return`{$SEED_SITE_HOSTNAME} encode zstd gzip @@ -72,8 +75,78 @@ reverse_proxy /.metrics* grafana:{$SEED_SITE_MONITORING_PORT:3001} reverse_proxy @ipfsget seed-daemon:{$HM_SITE_BACKEND_GRPCWEB_PORT:56001} reverse_proxy * seed-web:{$SEED_SITE_LOCAL_PORT:3000} -`}function Ix(x){return ib("sha256").update(x).digest("hex")}async function n1(x){let $=["seed-proxy","seed-web","seed-daemon"];for(let B of $)if(x.runSafe(`docker inspect ${B} --format '{{.State.Running}}' 2>/dev/null`)!=="true")return!1;return!0}async function Hx(x){let $=new Map,B=["seed-proxy","seed-web","seed-daemon"];for(let K of B){let S=x.runSafe(`docker inspect ${K} --format '{{.Image}}' 2>/dev/null`);if(S)$.set(K,S)}return $}function xb(x,$){let B=Wx(x.domain),K=x.testnet?"dev":"",S=x.testnet?$x:xx,Y={SEED_SITE_HOSTNAME:x.domain,SEED_SITE_DNS:B,SEED_SITE_TAG:x.release_channel,SEED_SITE_WORKSPACE:$.seedDir,SEED_UID:String(process.getuid()),SEED_GID:String(process.getgid()),SEED_LOG_LEVEL:x.compose_envs.LOG_LEVEL,SEED_IS_GATEWAY:String(x.gateway),SEED_ENABLE_STATISTICS:String(x.analytics),SEED_P2P_TESTNET_NAME:K,SEED_LIGHTNING_URL:S,NOTIFY_SERVICE_HOST:bx,SEED_SITE_MONITORING_WORKDIR:A($.seedDir,"monitoring")};return Object.entries(Y).map(([b,X])=>`${b}="${X}"`).join(" ")}function Vx(x){return[A(x.seedDir,"proxy"),A(x.seedDir,"proxy","data"),A(x.seedDir,"proxy","config"),A(x.seedDir,"web"),A(x.seedDir,"daemon"),A(x.seedDir,"monitoring"),A(x.seedDir,"monitoring","grafana"),A(x.seedDir,"monitoring","prometheus")]}async function $b(x,$){try{await D(x.seedDir)}catch{try{await O1(x.seedDir,{recursive:!0})}catch{L(`Creating ${x.seedDir} requires elevated permissions`),$.run(`sudo mkdir -p "${x.seedDir}"`),$.run(`sudo chown "$(id -u):$(id -g)" "${x.seedDir}"`)}}}async function u1(x,$,B,K){L("Deployment failed \u2014 rolling back to previous images...");for(let[Y,b]of x)L(` Restoring ${Y} to image ${b.slice(0,16)}...`),K.runSafe(`docker stop ${Y} 2>/dev/null`),K.runSafe(`docker rm ${Y} 2>/dev/null`);L(" Running docker compose up with cached images...");let S=xb($,B);K.runSafe(`${S} docker compose -f ${B.composePath} up -d --quiet-pull 2>&1`),L("Rollback complete. Check container status with: docker ps")}async function i1(x,$,B){let K=process.stdout.isTTY,S=K?a1():null,Y=(O)=>{if(S)S.message(O);L(O)};if(K)m.step("Starting deployment...");S?.start("Fetching docker-compose.yml..."),Y("Fetching docker-compose.yml...");let b=process.env.SEED_REPO_URL,X=b?`${b}/ops/docker-compose.yml`:x.compose_url,J=await fetch(X);if(!J.ok)throw S?.stop("Failed to fetch docker-compose.yml"),Error(`Failed to fetch compose file from ${X}: ${J.status}`);let Z=await J.text(),z=Ix(Z),q=await n1(B);if(x.compose_sha===z&&q){S?.stop("No changes detected \u2014 all containers healthy. Skipping redeployment."),L("No changes detected \u2014 compose SHA matches and containers are healthy. Skipping."),x.last_script_run=new Date().toISOString(),await t(x,$);return}if(x.compose_sha&&x.compose_sha!==z)Y(`Compose file changed: ${x.compose_sha.slice(0,8)} -> ${z.slice(0,8)}`);await $b($,B),await i($.composePath,Z,"utf-8"),Y("Setting up workspace directories...");let H=Vx($);for(let O of H)await O1(O,{recursive:!0});Y("Generating Caddyfile...");let N=Gx(x);await i(A($.seedDir,"proxy","CaddyFile"),N,"utf-8");let V=A($.seedDir,"web","config.json"),U=!1;try{await D(V)}catch{U=!0,await i(V,JSON.stringify({availableRegistrationSecret:x.link_secret})+` -`,"utf-8"),Y("Created initial web/config.json with registration secret.")}Y("Stopping any existing containers..."),B.runSafe("docker stop seed-site seed-daemon seed-proxy grafana prometheus 2>/dev/null"),B.runSafe("docker rm seed-site seed-daemon seed-proxy grafana prometheus 2>/dev/null");let M=await Hx(B);Y("Running docker compose up...");let P=xb(x,$);try{let O=`${P} docker compose -f ${$.composePath} up -d --pull always --quiet-pull`,E=await B.exec(O);if(E.stderr)L(`compose stderr: ${E.stderr}`)}catch(O){if(S?.stop("docker compose up failed"),L(`docker compose up failed: ${O}`),M.size>0)await u1(M,x,$,B);throw Error(`Deployment failed: ${O}`)}Y("Running post-deploy health checks...");let R=!1;for(let O=0;O<10;O++){if(await new Promise((E)=>setTimeout(E,3000)),R=await n1(B),R)break;Y(`Health check attempt ${O+1}/10...`)}if(!R){if(S?.stop("Health checks failed"),L("Health checks failed \u2014 containers not running after 30s"),M.size>0)await u1(M,x,$,B);throw Error("Deployment failed: containers did not become healthy within 30 seconds")}if(x.compose_sha=z,x.last_script_run=new Date().toISOString(),await t(x,$),S?.stop("Deployment complete!"),L("Deployment complete."),K&&U)f([`Your site is live at ${x.domain}`,"",` Secret: ${x.link_secret}`,"","Open the Seed desktop app and enter this secret to link","your publisher account to this site."].join(` -`),"Setup complete")}async function Ox(x,$){let B=`0 2 * * * /usr/bin/bun ${A(x.seedDir,"deploy.js")} >> ${x.deployLog} 2>&1 # seed-deploy`,K=$.runSafe("crontab -l 2>/dev/null")??"";if(K.includes("seed-deploy")){L("Cron job already installed. Skipping.");return}let Y=[K,B,"0 0,4,8,12,16,20 * * * docker image prune -a -f # seed-cleanup"].filter(Boolean).join(` +`}function j1($){return V9("sha256").update($).digest("hex")}async function Xb($){let K=["seed-proxy","seed-web","seed-daemon"];for(let Y of K)if($.runSafe(`docker inspect ${Y} --format '{{.State.Running}}' 2>/dev/null`)!=="true")return!1;return!0}async function k9($){let K=new Map,Y=["seed-proxy","seed-web","seed-daemon"];for(let Q of Y){let X=$.runSafe(`docker inspect ${Q} --format '{{.Image}}' 2>/dev/null`);if(X)K.set(Q,X)}return K}function y1($,K){let Y=Vb($.domain),Q=$.testnet?"dev":"",X=$.testnet?P9:T9,x={SEED_SITE_HOSTNAME:$.domain,SEED_SITE_DNS:Y,SEED_SITE_TAG:$.release_channel,SEED_SITE_WORKSPACE:K.seedDir,SEED_UID:String(process.getuid()),SEED_GID:String(process.getgid()),SEED_LOG_LEVEL:$.compose_envs.LOG_LEVEL,SEED_IS_GATEWAY:String($.gateway),SEED_ENABLE_STATISTICS:String($.analytics),SEED_P2P_TESTNET_NAME:Q,SEED_LIGHTNING_URL:X,NOTIFY_SERVICE_HOST:A9,SEED_SITE_MONITORING_WORKDIR:A(K.seedDir,"monitoring")};return Object.entries(x).map(([b,Z])=>`${b}="${Z}"`).join(" ")}function y9($){return[A($.seedDir,"proxy"),A($.seedDir,"proxy","data"),A($.seedDir,"proxy","config"),A($.seedDir,"web"),A($.seedDir,"daemon"),A($.seedDir,"monitoring"),A($.seedDir,"monitoring","grafana"),A($.seedDir,"monitoring","prometheus")]}async function w1($,K){try{await F($.seedDir)}catch{try{await Z1($.seedDir,{recursive:!0})}catch{I(`Creating ${$.seedDir} requires elevated permissions`),K.run(`sudo mkdir -p "${$.seedDir}"`),K.run(`sudo chown "$(id -u):$(id -g)" "${$.seedDir}"`)}}}async function xb($,K,Y,Q){I("Deployment failed \u2014 rolling back to previous images...");for(let[x,b]of $)I(` Restoring ${x} to image ${b.slice(0,16)}...`),Q.runSafe(`docker stop ${x} 2>/dev/null`),Q.runSafe(`docker rm ${x} 2>/dev/null`);I(" Running docker compose up with cached images...");let X=y1(K,Y);Q.runSafe(`${X} docker compose -f ${Y.composePath} up -d --quiet-pull 2>&1`),I("Rollback complete. Check container status with: docker ps")}async function w9($){let K=process.argv[1],Y=`${k1}/dist/deploy.js`;try{let Q=await fetch(Y);if(!Q.ok){I(`Self-update: failed to fetch ${Y}: ${Q.status}`);return}let X=await Q.text(),x="";try{x=await R1(K,"utf-8")}catch{}if(j1(X)!==j1(x))await u(K,X,"utf-8"),I("Self-update: deploy.js updated (takes effect on next run).");else I("Self-update: deploy.js is up to date.")}catch(Q){I(`Self-update: skipped (${Q})`)}}async function K1($,K,Y){let Q=process.stdout.isTTY,X=Q?Yb():null,x=(O)=>{if(X)X.message(O);I(O)};if(Q)U.step("Starting deployment...");X?.start("Fetching docker-compose.yml..."),x("Fetching docker-compose.yml...");let Z=process.env.SEED_DEPLOY_URL||process.env.SEED_REPO_URL?`${k1}/docker-compose.yml`:$.compose_url,G=await fetch(Z);if(!G.ok)throw X?.stop("Failed to fetch docker-compose.yml"),Error(`Failed to fetch compose file from ${Z}: ${G.status}`);let z=await G.text(),J=j1(z),B=await Xb(Y);if($.compose_sha===J&&B){if(X?.stop("No changes detected \u2014 all containers healthy. Skipping redeployment."),I("No changes detected \u2014 compose SHA matches and containers are healthy. Skipping."),Q)console.log(` + To change your node's configuration, run 'seed-deploy deploy --reconfigure'. +`);$.last_script_run=new Date().toISOString(),await Q1($,K);return}if($.compose_sha&&$.compose_sha!==J)x(`Compose file changed: ${$.compose_sha.slice(0,8)} -> ${J.slice(0,8)}`);await w1(K,Y),await u(K.composePath,z,"utf-8"),x("Setting up workspace directories...");let W=y9(K);for(let O of W)await Z1(O,{recursive:!0});x("Generating Caddyfile...");let V=R9($);await u(A(K.seedDir,"proxy","CaddyFile"),V,"utf-8");let M=A(K.seedDir,"web","config.json"),N=!1;try{await F(M)}catch{N=!0,await u(M,JSON.stringify({availableRegistrationSecret:$.link_secret})+` +`,"utf-8"),x("Created initial web/config.json with registration secret.")}let j=await k9(Y),H=y1($,K);if(!$.compose_sha)x("Removing legacy containers..."),Y.runSafe("docker stop seed-site seed-daemon seed-web seed-proxy autoupdater grafana prometheus 2>/dev/null"),Y.runSafe("docker rm seed-site seed-daemon seed-web seed-proxy autoupdater grafana prometheus 2>/dev/null");x("Pulling latest images...");try{await Y.exec(`${H} docker compose -f ${K.composePath} pull --quiet`)}catch(O){I(`Image pull failed: ${O}`)}x("Recreating containers...");try{let O=`${H} docker compose -f ${K.composePath} up -d --quiet-pull`,d=await Y.exec(O);if(d.stderr)I(`compose stderr: ${d.stderr}`)}catch(O){if(X?.stop("docker compose up failed"),I(`docker compose up failed: ${O}`),j.size>0)await xb(j,$,K,Y);throw Error(`Deployment failed: ${O}`)}let L=!1;for(let O=0;O<10;O++)if(x(`Health check ${O+1}/10...`),await new Promise((d)=>setTimeout(d,3000)),L=await Xb(Y),L)break;if(!L){if(X?.stop("Health checks failed"),I("Health checks failed \u2014 containers not running after 30s"),j.size>0)await xb(j,$,K,Y);throw Error("Deployment failed: containers did not become healthy within 30 seconds")}if($.compose_sha=J,$.last_script_run=new Date().toISOString(),await Q1($,K),x("Cleaning up unused images..."),Y.runSafe('docker image prune -a -f --filter "until=1m" 2>/dev/null'),X?.stop("Deployment complete!"),I("Deployment complete."),Q)U.message(x1);if(Q&&N)E([`Your site is live at ${$.domain}`,"",` Secret: ${$.link_secret}`,"","Open the Seed desktop app and enter this secret to link","your publisher account to this site."].join(` +`),"Setup complete")}function Hb($,K,Y="/usr/local/bin/bun"){let Q=`0 2 * * * ${Y} ${A(K.seedDir,"deploy.js")} >> ${K.deployLog} 2>&1 # seed-deploy`,X='0 0,4,8,12,16,20 * * * docker image prune -a -f --filter "until=1h" # seed-cleanup';return[$.split(` +`).filter((b)=>!b.includes("# seed-deploy")&&!b.includes("# seed-cleanup")).join(` +`).trim(),Q,'0 0,4,8,12,16,20 * * * docker image prune -a -f --filter "until=1h" # seed-cleanup'].filter(Boolean).join(` `)+` -`;try{D1(`echo '${Y}' | crontab -`,{encoding:"utf-8"}),L("Installed nightly deployment cron job (02:00) and image cleanup cron.")}catch(b){L(`Warning: Failed to install cron job: ${b}`)}}async function Ax(){let x=Bx(),$=Kx();if(await $b(x,$),await Sx(x)){L(`Seed deploy v${A1} \u2014 config found at ${x.configPath}, running headless.`);let b=await Yx(x);await i1(b,x,$);return}let K=await Zx($),S;if(K)S=await qx(K,x,$);else S=await zx(x);let Y=await k({message:"Install nightly cron job for automatic updates? (runs at 02:00)",initialValue:!0});if(!y(Y)&&Y)await Ox(x,$),m.success("Cron job installed. Your node will auto-update nightly at 02:00.");await i1(S,x,$),o1("Setup complete! Your Seed node is running.")}if(import.meta.main)Ax().catch((x)=>{console.error("Fatal error:",x),process.exit(1)});export{t as writeConfig,Ix as sha256,Ox as setupCron,Yx as readConfig,Xx as parseWebEnv,Jx as parseImageTag,Qx as parseDaemonEnv,Kx as makeShellRunner,Bx as makePaths,L as log,Vx as getWorkspaceDirs,Hx as getContainerImages,bb as generateSecret,Gx as generateCaddyfile,Wx as extractDns,e1 as environmentPresets,$b as ensureSeedDir,Zx as detectOldInstall,i1 as deploy,Sx as configExists,n1 as checkContainersHealthy,xb as buildComposeEnv,A1 as VERSION,bx as NOTIFY_SERVICE_HOST,$x as LIGHTNING_URL_TESTNET,xx as LIGHTNING_URL_MAINNET,sb as DEFAULT_SEED_DIR,eb as DEFAULT_REPO_URL,s1 as DEFAULT_COMPOSE_URL}; +`}async function Mb($,K){let Y=K.runSafe("which bun")??"/usr/local/bin/bun",Q=K.runSafe("crontab -l 2>/dev/null")??"",X=Hb(Q,$,Y);try{if(K.run(`echo '${X}' | crontab -`),Q.includes("# seed-deploy")||Q.includes("# seed-cleanup"))I("Updated existing seed cron jobs.");else I("Installed nightly deployment cron job (02:00) and image cleanup cron.")}catch(x){I(`Warning: Failed to install cron job: ${x}`)}}var C9=["deploy","stop","start","restart","status","config","logs","cron","backup","restore","uninstall"];function E9($=process.argv){let K=$.slice(2),Y=K[0]??"deploy";if(Y==="--help"||Y==="-h")return{command:"help",args:[]};if(Y==="--version"||Y==="-v")return{command:"version",args:[]};if(Y==="--reconfigure")return{command:"deploy",args:[],reconfigure:!0};if(C9.includes(Y)){let Q=K.slice(1),X=Y==="deploy"&&Q.includes("--reconfigure"),x=X?Q.filter((b)=>b!=="--reconfigure"):Q;return{command:Y,args:x,reconfigure:X}}console.error(`Unknown command: ${Y} +`),Ob(),process.exit(1)}function Ob(){let $=` +Seed Node Deployment v${C} + +Usage: seed-deploy [command] [options] + +Commands: + deploy Deploy or update the Seed node (default) + stop Stop and remove all Seed containers + start Start containers without re-deploying + restart Restart all Seed containers + status Show node health, versions, and connectivity + config Print current configuration (secrets redacted) + logs Tail container logs [daemon|web|proxy] + cron Install or remove automatic update cron jobs + backup Create a portable backup of all node data + restore Restore node data from a backup file + uninstall Remove all Seed containers, data, and configuration + +Options: + --reconfigure Re-run the setup wizard to change configuration + -h, --help Show this help message + -v, --version Show script version + +Examples: + seed-deploy Deploy or update + seed-deploy deploy --reconfigure Change node configuration + seed-deploy stop Teardown containers + seed-deploy status Check node health + seed-deploy logs daemon Tail seed-daemon logs + seed-deploy cron Install automatic update cron + seed-deploy cron remove Remove cron jobs + seed-deploy backup Create backup + seed-deploy backup /tmp/backup.tar.gz Create backup at custom path + seed-deploy restore backup.tar.gz Restore from backup file + +The 'seed-deploy' command is installed at ~/.local/bin/seed-deploy +during initial setup. The deployment script lives at ${Bb}/deploy.js. +`.trimStart();console.log($)}async function m9($,K,Y=!1){if(await w1($,K),!process.stdout.isTTY)await w9($);if(await D($)){if(Y&&process.stdout.isTTY){let Z=await v($),G=await Qb($,Z);await K1(G,$,K),h(`Reconfiguration complete! Your Seed node is running. +${x1}`);return}I(`Seed deploy v${C} \u2014 config found at ${$.configPath}, running headless.`);let b=await v($);await K1(b,$,K);return}let Q=await j9(K),X;if(Q)X=await Gb(Q,$,K);else X=await Qb($);let x=await w({message:"Install nightly cron job for automatic updates? (runs at 02:00)",initialValue:!0});if(!k(x)&&x)await Mb($,K),U.success("Cron job installed. Your node will auto-update nightly at 02:00.");await K1(X,$,K),h(`Setup complete! Your Seed node is running. +${x1}`)}async function Ab($,K){console.log("Stopping and removing Seed containers..."),K.runSafe(`docker compose -f "${$.composePath}" down`),console.log("All Seed containers stopped and removed.")}async function Tb($,K){if(!await D($))console.error(`No config found at ${$.configPath}. Run 'seed-deploy' first to set up.`),process.exit(1);let Y=await v($),Q=y1(Y,$);console.log("Starting Seed containers..."),K.run(`${Q} docker compose -f "${$.composePath}" up -d --quiet-pull`),console.log("Seed containers started.")}async function v9($,K){await Ab($,K),await Tb($,K)}async function f9($,K){console.log(` +Seed Node Status v${C}`),console.log("\u2501".repeat(40));let Y=null;if(await D($))Y=await v($),console.log(` +Configuration:`),console.log(` Domain: ${Y.domain}`),console.log(` Environment: ${{prod:"Production",staging:"Staging",dev:"Development"}[Y.environment]}`),console.log(` Channel: ${Y.release_channel}`),console.log(` Gateway: ${Y.gateway?"Yes":"No"}`),console.log(` Analytics: ${Y.analytics?"Yes":"No"}`),console.log(` Config: ${$.configPath}`);else console.log(` +No config found at ${$.configPath}. Node is not set up.`);console.log(` +Containers:`);let Q=["seed-daemon","seed-web","seed-proxy"],X=!1;for(let B of Q){let W=K.runSafe(`docker inspect ${B} --format '{{.State.Status}}' 2>/dev/null`),V=K.runSafe(`docker inspect ${B} --format '{{.Config.Image}}' 2>/dev/null`),M=K.runSafe(`docker inspect ${B} --format '{{.State.StartedAt}}' 2>/dev/null`);if(W){if(console.log(` ${W==="running"?"\u2714":"\u26A0"} ${B.padEnd(14)} ${(W??"").padEnd(10)} ${V??""}${M?` (since ${M})`:""}`),W!=="running"){X=!0;let j=K.runSafe(`docker logs --tail 1 ${B} 2>&1`);if(j)console.log(` \u2514 ${j.slice(0,120)}`)}}else console.log(` \u2718 ${B.padEnd(14)} not found`)}if(X)console.log(` + Tip: Check logs with 'seed-deploy logs daemon|web|proxy'`);let x=K.runSafe("docker inspect prometheus --format '{{.State.Status}}' 2>/dev/null"),b=K.runSafe("docker inspect grafana --format '{{.State.Status}}' 2>/dev/null");if(x||b){let B=A($.seedDir,"monitoring","prometheus","prometheus.yaml"),W=A($.seedDir,"monitoring","grafana","provisioning"),V=!0;console.log(` +Monitoring:`);try{await F(B),console.log(" \u2714 Prometheus config exported")}catch{V=!1,console.log(" \u26A0 Prometheus config not exported")}try{await F(W),console.log(" \u2714 Grafana provisioning exported")}catch{V=!1,console.log(" \u26A0 Grafana provisioning not exported")}if(!V)console.log(` + Tip: This may indicate a permissions issue with the monitoring/ directory.`),console.log(" Run 'seed-deploy deploy' to attempt an automatic fix.")}if(Y){console.log(` +Health Checks:`);let B=Vb(Y.domain),W=K.runSafe(`curl -sSf -o /dev/null -w '%{http_code}' --max-time 10 "${Y.domain}" 2>/dev/null`);if(W&&W.startsWith("2"))console.log(` \u2714 HTTPS ${W} OK`);else if(W)console.log(` \u26A0 HTTPS ${W}`);else console.log(" \u26A0 HTTPS unreachable");let V=K.runSafe("curl -s --max-time 5 ifconfig.me 2>/dev/null"),M=K.runSafe(`dig +short ${B} A 2>/dev/null | head -1`);if(V&&M)if(M.trim()===V.trim())console.log(` \u2714 DNS ${B} -> ${M} (matches public IP)`);else console.log(` \u26A0 DNS ${B} -> ${M} (public IP is ${V})`);else if(!M)console.log(` \u26A0 DNS ${B} does not resolve`);else console.log(" ? DNS could not determine public IP");let N=K.runSafe(`echo | openssl s_client -servername "${B}" -connect "${B}:443" 2>/dev/null | openssl x509 -noout -enddate 2>/dev/null`);if(N){let j=N.replace("notAfter=",""),H=new Date(j),L=Math.floor((H.getTime()-Date.now())/86400000),O=L>14?"\u2714":"\u26A0";console.log(` ${O} Certificate valid, expires ${H.toISOString().slice(0,10)} (${L}d)`)}else console.log(" \u26A0 Certificate could not check")}let Z=K.runSafe(`du -sh "${$.seedDir}" 2>/dev/null`);if(Z)console.log(` +Disk:`),console.log(` ${$.seedDir} ${Z.split("\t")[0]}`);let G=K.runSafe("crontab -l 2>/dev/null")??"",z=G.split(` +`).find((B)=>B.includes("# seed-deploy")),J=G.split(` +`).find((B)=>B.includes("# seed-cleanup"));if(console.log(` +Cron:`),console.log(` Auto-update: ${z?z.split(" ").slice(0,5).join(" "):"not installed"}`),console.log(` Cleanup: ${J?J.split(" ").slice(0,5).join(" "):"not installed"}`),!z||!J)console.log(` + Tip: Run 'seed-deploy cron' to set up automatic updates.`);console.log("")}async function F9($){if(!await D($))console.error(`No config found at ${$.configPath}. Run 'seed-deploy' first.`),process.exit(1);let Y={...await v($),link_secret:"****"};console.log(JSON.stringify(Y,null,2))}async function d9($,K){let Y=K[0],Q=Y?`seed-${Y}`:"";try{Zb(`docker compose -f "${$.composePath}" logs -f --tail 100 ${Q}`,{stdio:"inherit"})}catch{}}async function g9($,K,Y){let Q=Y[0]??"install";if(Q==="remove"){let X=K.runSafe("crontab -l 2>/dev/null")??"";if(!X.includes("# seed-deploy")&&!X.includes("# seed-cleanup")){console.log("No seed cron jobs found. Nothing to remove.");return}let x=Pb(X);try{K.run(`echo '${x}' | crontab -`),console.log("Seed cron jobs removed.")}catch(b){console.error(`Failed to remove cron jobs: ${b}`),process.exit(1)}return}if(Q==="install"){await Mb($,K);let X=K.runSafe("crontab -l 2>/dev/null")??"",x=X.split(` +`).find((Z)=>Z.includes("# seed-deploy")),b=X.split(` +`).find((Z)=>Z.includes("# seed-cleanup"));console.log("Cron jobs installed:"),console.log(` Auto-update: ${x??"(missing)"}`),console.log(` Cleanup: ${b??"(missing)"}`);return}console.error(`Unknown cron subcommand: ${Q}`),console.error("Usage: seed-deploy cron [install|remove]"),process.exit(1)}function c9($){return $.split(` +`).filter((K)=>K.includes("# seed-deploy")||K.includes("# seed-cleanup"))}function Pb($){return $.split(` +`).filter((K)=>!K.includes("# seed-deploy")&&!K.includes("# seed-cleanup")).join(` +`).trim()+` +`}async function Sb($,K,Y){if(!await D($))console.error(`No config found at ${$.configPath}. Nothing to back up.`),process.exit(1);let Q=await v($),X=new Date().toISOString().replace(/[:.]/g,"-"),x=A($.seedDir,"backups",`seed-backup-${X}.tar.gz`),b=Y[0]||x,Z=Y1(b);await Z1(Z,{recursive:!0});let G=K.runSafe("crontab -l 2>/dev/null")??"",z={version:C,timestamp:new Date().toISOString(),hostname:Q.domain,seedDir:$.seedDir,cron:c9(G)};await u(A($.seedDir,"backup-meta.json"),JSON.stringify(z,null,2)+` +`,"utf-8"),console.log("Stopping containers for consistent backup..."),K.runSafe(`docker compose -f "${$.composePath}" stop`);let J=qb($.seedDir),B=Y1($.seedDir);try{K.run(`tar -czf "${b}" -C "${B}" --exclude="${J}/backups" --exclude="${J}/.env" --exclude="${J}/deploy.js" --exclude="${J}/deploy.log" "${J}/config.json" "${J}/backup-meta.json" "${J}/docker-compose.yml" "${J}/web" "${J}/daemon" "${J}/proxy"`)}catch(V){console.error(`Backup failed: ${V}`),K.runSafe(`docker compose -f "${$.composePath}" start`),process.exit(1)}K.runSafe(`rm -f "${A($.seedDir,"backup-meta.json")}"`),console.log("Restarting containers..."),K.runSafe(`docker compose -f "${$.composePath}" start`);let W=K.runSafe(`du -h "${b}"`)?.split("\t")[0]??"unknown";console.log(` +Backup created: ${b} (${W})`)}async function u9($,K,Y){let Q=Y[0];if(!Q)console.error("Usage: seed-deploy restore "),process.exit(1);try{await F(Q)}catch{console.error(`File not found: ${Q}`),process.exit(1)}let X=null,x=qb($.seedDir),b=K.runSafe(`tar -xzf "${Q}" -O "${x}/backup-meta.json" 2>/dev/null`);if(b)try{X=JSON.parse(b)}catch{}if(o(`Seed Node Restore v${C}`),X)E([`Created: ${X.timestamp}`,`Source: ${X.hostname}`,`Version: ${X.version}`].join(` +`),"Restoring from backup");let Z=await w({message:`This will overwrite all data in ${$.seedDir}. Continue?`});if(k(Z)||!Z)f("Restore cancelled."),console.log(` +Run 'seed-deploy restore ' to try again. +`),process.exit(0);console.log("Stopping existing containers..."),K.runSafe(`docker compose -f "${$.composePath}" down`),console.log("Extracting backup..."),await w1($,K);let G=Y1($.seedDir);if(K.run(`tar -xzf "${Q}" -C "${G}"`),K.runSafe(`rm -f "${A($.seedDir,"backup-meta.json")}"`),X?.cron&&X.cron.length>0){let B=K.runSafe("crontab -l 2>/dev/null")??"",W=Hb(B,$);try{K.run(`echo '${W}' | crontab -`),console.log("Cron jobs restored.")}catch{console.log("Warning: Could not restore cron jobs.")}}let z=await w({message:"Would you like to review the configuration before deploying?",initialValue:!1}),J;if(!k(z)&&z){let B=await v($),W={workspace:$.seedDir,secret:B.link_secret,secretConsumed:!1,hostname:B.domain,logLevel:B.compose_envs.LOG_LEVEL,imageTag:B.release_channel,testnet:B.testnet,gateway:B.gateway,trafficStats:B.analytics};J=await Gb(W,$,K)}else J=await v($);await K1(J,$,K),h(`Restore complete! Your Seed node is running. +${x1}`)}async function p9($,K){o(`Seed Node Uninstall v${C}`),E(["This will permanently delete:"," - All Seed containers",` - All node data at ${$.seedDir}/ (daemon identity, web data, config)`," - Cron jobs for seed-deploy and seed-cleanup","","This action is IRREVERSIBLE."].join(` +`),"Warning");let Y=await w({message:"Would you like to create a backup before uninstalling?",initialValue:!0});if(!k(Y)&&Y)await Sb($,K,[]);let Q=await c({message:'Type "yes" to confirm uninstallation:',validate:(x)=>{if(x!=="yes")return'Please type "yes" to confirm, or press Ctrl+C to cancel.'}});if(k(Q))f("Uninstall cancelled."),process.exit(0);console.log("Stopping and removing containers..."),K.runSafe(`docker compose -f "${$.composePath}" down`);let X=K.runSafe("crontab -l 2>/dev/null")??"";if(X.includes("# seed-deploy")||X.includes("# seed-cleanup")){let x=Pb(X);try{K.run(`echo '${x}' | crontab -`),console.log("Cron jobs removed.")}catch{console.log("Warning: Could not remove cron jobs.")}}console.log(`Removing ${$.seedDir}...`);try{K.run(`rm -rf "${$.seedDir}"`)}catch{console.log(`Could not remove ${$.seedDir}. Trying with sudo...`),K.run(`sudo rm -rf "${$.seedDir}"`)}h("Seed node uninstalled.")}async function r9(){let{command:$,args:K,reconfigure:Y}=E9(),Q=S9(),X=I9();switch($){case"help":Ob();return;case"version":console.log(C);return;case"deploy":return m9(Q,X,Y);case"stop":return Ab(Q,X);case"start":return Tb(Q,X);case"restart":return v9(Q,X);case"status":return f9(Q,X);case"config":return F9(Q);case"logs":return d9(Q,K);case"cron":return g9(Q,X,K);case"backup":return Sb(Q,X,K);case"restore":return u9(Q,X,K);case"uninstall":return p9(Q,X)}}if(import.meta.main)r9().catch(($)=>{console.error("Fatal error:",$),process.exit(1)});export{Q1 as writeConfig,j1 as sha256,Mb as setupCron,w9 as selfUpdate,Pb as removeSeedCronLines,v as readConfig,Ob as printHelp,_9 as parseWebEnv,N9 as parseImageTag,U9 as parseDaemonEnv,E9 as parseArgs,I9 as makeShellRunner,S9 as makePaths,I as log,L9 as inferEnvironment,y9 as getWorkspaceDirs,k9 as getContainerImages,zb as generateSecret,R9 as generateCaddyfile,c9 as extractSeedCronLines,Vb as extractDns,Wb as environmentPresets,w1 as ensureSeedDir,j9 as detectOldInstall,K1 as deploy,D as configExists,Xb as checkContainersHealthy,Hb as buildCrontab,y1 as buildComposeEnv,C as VERSION,k1 as OPS_BASE_URL,A9 as NOTIFY_SERVICE_HOST,P9 as LIGHTNING_URL_TESTNET,T9 as LIGHTNING_URL_MAINNET,Bb as DEFAULT_SEED_DIR,O9 as DEFAULT_REPO_URL,Jb as DEFAULT_COMPOSE_URL}; diff --git a/ops/docker-compose.yml b/ops/docker-compose.yml index 825d2c9a3..1d499a3bc 100644 --- a/ops/docker-compose.yml +++ b/ops/docker-compose.yml @@ -5,6 +5,7 @@ services: proxy: container_name: seed-proxy image: caddy:2 + user: "${SEED_UID}:${SEED_GID}" depends_on: - seed-daemon - seed-web @@ -20,9 +21,9 @@ services: - "SEED_SITE_BACKEND_GRPCWEB_PORT=${SEED_SITE_BACKEND_GRPCWEB_PORT:-56001}" - "SEED_SITE_LOCAL_PORT=${SEED_SITE_LOCAL_PORT:-3000}" volumes: - - ${SEED_SITE_WORKSPACE}/proxy/data:/data - - ${SEED_SITE_WORKSPACE}/proxy/config:/config - - ${SEED_SITE_WORKSPACE}/proxy/CaddyFile:/etc/caddy/Caddyfile + - ${SEED_SITE_WORKSPACE}/proxy/data:/data:z + - ${SEED_SITE_WORKSPACE}/proxy/config:/config:z + - ${SEED_SITE_WORKSPACE}/proxy/CaddyFile:/etc/caddy/Caddyfile:z seed-web: container_name: seed-web @@ -36,7 +37,7 @@ services: - "${SEED_SITE_LOCAL_PORT:-3000}:${SEED_SITE_LOCAL_PORT:-3000}" restart: unless-stopped volumes: - - ${SEED_SITE_WORKSPACE}/web:/data:rw + - ${SEED_SITE_WORKSPACE}/web:/data:rw,z environment: - "SEED_BASE_URL=${SEED_SITE_HOSTNAME}" - "NOTIFY_SERVICE_HOST=${NOTIFY_SERVICE_HOST}" @@ -49,6 +50,7 @@ services: seed-daemon: container_name: seed-daemon image: seedhypermedia/site:${SEED_SITE_TAG:-latest} + user: "${SEED_UID}:${SEED_GID}" restart: unless-stopped ports: - "56000:56000" @@ -61,12 +63,12 @@ services: - "LIGHTNING_API_URL=${SEED_LIGHTNING_URL}" - "SENTRY_DSN=${SEED_SITE_SENTRY_DSN:-https://47c66bd7a6d64db68a59c03f2337e475@o4504088793841664.ingest.sentry.io/4505527493328896}" volumes: - - ${SEED_SITE_WORKSPACE}/daemon:/data:rw - - ${SEED_SITE_MONITORING_WORKDIR:-./monitoring}/grafana:/exported_grafana:rw - - ${SEED_SITE_MONITORING_WORKDIR:-./monitoring}/prometheus:/exported_prometheus:rw + - ${SEED_SITE_WORKSPACE}/daemon:/data:rw,z + - ${SEED_SITE_MONITORING_WORKDIR:-./monitoring}/grafana:/exported_grafana:rw,z + - ${SEED_SITE_MONITORING_WORKDIR:-./monitoring}/prometheus:/exported_prometheus:rw,z command: > - sh -c "rsync -a /monitoring/prometheus/ /exported_prometheus && - rsync -a /monitoring/grafana/ /exported_grafana && + sh -c "(rsync -rlt /monitoring/prometheus/ /exported_prometheus 2>&1 || echo '[warn] monitoring/prometheus export failed β€” check directory permissions') && + (rsync -rlt /monitoring/grafana/ /exported_grafana 2>&1 || echo '[warn] monitoring/grafana export failed β€” check directory permissions') && seed-daemon -data-dir=/data -lndhub.mainnet -p2p.port=56000 --http.port=${SEED_SITE_BACKEND_GRPCWEB_PORT:-56001} -grpc.port=56002 -p2p.no-relay=true -p2p.force-reachability-public=true -syncing.smart=true -syncing.no-sync-back=true -syncing.no-pull=${SEED_SITE_NO_PULL:-false} -p2p.announce-addrs=/dns4/${SEED_SITE_DNS}/tcp/56000,/dns4/${SEED_SITE_DNS}/udp/56000/quic-v1 ${SEED_SITE_HOSTNAME}" prometheus: @@ -83,7 +85,7 @@ services: - "host.docker.internal:host-gateway" volumes: - prometheus-data:/prometheus - - ${SEED_SITE_MONITORING_WORKDIR:-./monitoring}/prometheus/prometheus.yaml:/etc/prometheus/prometheus.yml:ro + - ${SEED_SITE_MONITORING_WORKDIR:-./monitoring}/prometheus/prometheus.yaml:/etc/prometheus/prometheus.yml:ro,z grafana: image: grafana/grafana:main @@ -97,8 +99,8 @@ services: - internal_network volumes: - grafana-data:/var/lib/grafana - - ${SEED_SITE_MONITORING_WORKDIR:-./monitoring}/grafana/dashboards:/etc/grafana/dashboards:ro - - ${SEED_SITE_MONITORING_WORKDIR:-./monitoring}/grafana/provisioning:/etc/grafana/provisioning:ro + - ${SEED_SITE_MONITORING_WORKDIR:-./monitoring}/grafana/dashboards:/etc/grafana/dashboards:ro,z + - ${SEED_SITE_MONITORING_WORKDIR:-./monitoring}/grafana/provisioning:/etc/grafana/provisioning:ro,z environment: GF_LOG_MODE: console GF_PATHS_PROVISIONING: "/etc/grafana/provisioning" From 24a1ebe443d16d26999fdd26f2addf2cfb9a02a8 Mon Sep 17 00:00:00 2001 From: juligasa <11684004+juligasa@users.noreply.github.com> Date: Thu, 19 Feb 2026 17:55:08 +0100 Subject: [PATCH 5/5] docs(ops): add deployment system README --- ops/README.md | 226 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 ops/README.md diff --git a/ops/README.md b/ops/README.md new file mode 100644 index 000000000..007b9073e --- /dev/null +++ b/ops/README.md @@ -0,0 +1,226 @@ +# Seed Node Deployment + +Self-hosted deployment system for Seed nodes. A single script handles +first-time setup, configuration, container orchestration, backups, +and automatic updates. + +## Quick Start + +```sh +curl -fsSL https://raw.githubusercontent.com/seed-hypermedia/seed/main/ops/deploy.sh | sh +``` + +The bootstrap script installs Docker and Bun (if missing), downloads the +deployment engine, and launches an interactive wizard to configure your +node. After setup you manage everything through the `seed-deploy` CLI. + +## Architecture + +``` +deploy.sh Minimal bootstrap β€” installs Docker + Bun, downloads deploy.js + | + v +deploy.ts Main deployment engine (bundled to dist/deploy.js) + | - Interactive wizard (first run) + | - Headless deploy (subsequent runs / cron) + | - Full CLI for node management + v +docker-compose.yml Container definitions for proxy, web, and daemon +``` + +### Files + +| File | Purpose | +| -------------------- | ---------------------------------------- | +| `deploy.sh` | One-line bootstrap installer | +| `deploy.ts` | Deployment engine source (TypeScript) | +| `deploy.test.ts` | Test suite (115 tests, 257 assertions) | +| `dist/deploy.js` | Committed production bundle (Bun target) | +| `docker-compose.yml` | Docker Compose service definitions | + +## Modes of Operation + +### 1. Interactive Wizard (first run) + +When no `config.json` exists, the script launches a terminal wizard: + +1. **Public hostname** β€” the `https://` URL for the node (required) +2. **Environment** β€” Production / Staging / Development +3. **Log level** β€” Debug / Info / Warn / Error +4. **Gateway mode** β€” whether the node serves all known public content +5. **Analytics** β€” enable Plausible.io traffic dashboard +6. **Contact email** β€” optional, for security update notifications + +The wizard also detects legacy installations (from `website_deployment.sh`) +and offers a migration path, pre-filling values from the old config. + +### 2. Headless Deploy (subsequent runs) + +When `config.json` exists, the script runs without prompts: + +1. Self-updates `deploy.js` from the upstream repo (cron only) +2. Fetches `docker-compose.yml` and compares SHA-256 with the stored hash +3. If nothing changed and all containers are healthy, skips redeployment +4. Otherwise: pulls images first (while old containers serve traffic), + then recreates containers from cache β€” minimizes downtime +5. Prunes unused Docker images after successful deploy + +### 3. Reconfiguration + +```sh +seed-deploy deploy --reconfigure +``` + +Re-runs the wizard with current values shown as placeholders. Press Tab +to keep a value, type to change it, or press Enter to clear optional +fields. Changed fields are marked with a pencil icon in the summary. + +## CLI Reference + +``` +seed-deploy [command] [options] +``` + +| Command | Description | +| ---------------- | ---------------------------------------------------------- | +| `deploy` | Deploy or update the node (default when no command given) | +| `stop` | Stop and remove all containers | +| `start` | Start containers without re-deploying | +| `restart` | Restart all containers | +| `status` | Show health, versions, connectivity, disk, and cron status | +| `config` | Print current configuration (secrets redacted) | +| `logs [service]` | Tail container logs (`daemon`, `web`, or `proxy`) | +| `cron [remove]` | Install or remove automatic update cron jobs | +| `backup [path]` | Create a portable backup of all node data | +| `restore ` | Restore node data from a backup archive | +| `uninstall` | Remove all containers, data, and configuration | + +| Option | Description | +| ----------------- | ----------------------------------------------- | +| `--reconfigure` | Re-run the setup wizard to change configuration | +| `-h`, `--help` | Show help message | +| `-v`, `--version` | Show script version | + +## Environment Presets + +A single "Environment" choice controls multiple settings: + +| Environment | Image Tag | Network | Use Case | +| --------------- | --------- | ------- | ------------------------------------------- | +| **Production** | `latest` | Mainnet | Stable releases (recommended) | +| **Staging** | `dev` | Mainnet | Testing development builds on real network | +| **Development** | `dev` | Testnet | Development builds on isolated test network | + +## Configuration + +Stored at `/config.json`. User-facing fields: + +| Field | Required | Description | +| ------------- | -------- | ---------------------------------------- | +| `domain` | Yes | Public hostname including `https://` | +| `email` | No | Contact email for security notifications | +| `environment` | Yes | `prod`, `staging`, or `dev` | +| `gateway` | Yes | Serve all known public content | +| `analytics` | Yes | Enable Plausible.io web analytics | + +Internal fields (managed by the script): `compose_url`, `compose_sha`, +`compose_envs`, `release_channel`, `testnet`, `link_secret`, +`last_script_run`. + +## Docker Services + +Three core containers, plus optional metrics: + +| Container | Image | Ports | Purpose | +| ------------- | --------------------- | ---------------- | ------------------------------- | +| `seed-proxy` | `caddy:2` | 80, 443, 443/udp | Reverse proxy + auto TLS | +| `seed-web` | `seedhypermedia/web` | 3000 | Web frontend | +| `seed-daemon` | `seedhypermedia/site` | 56000, 56000/udp | P2P daemon + API | +| `prometheus` | `prom/prometheus` | β€” | Metrics (profile: `metrics`) | +| `grafana` | `grafana/grafana` | β€” | Dashboards (profile: `metrics`) | + +All containers run as the host user (`SEED_UID:SEED_GID`) β€” no root inside +containers. All bind mounts use the `:z` flag for SELinux compatibility. + +## Automatic Updates + +The cron system installs two jobs: + +| Schedule | Task | +| ----------------- | -------------------------------------------------------------------------------------------- | +| **02:00 daily** | Run `deploy.js` β€” self-updates the script, pulls new images, recreates containers if changed | +| **Every 4 hours** | `docker image prune -a -f --filter "until=1h"` β€” removes unused images older than 1 hour | + +Install with `seed-deploy cron`, remove with `seed-deploy cron remove`. + +## Backup & Restore + +**Backup** creates a `.tar.gz` containing `config.json`, `docker-compose.yml`, +and the `web/`, `daemon/`, and `proxy/` data directories. Containers are +stopped during backup for data consistency and restarted after. + +```sh +seed-deploy backup # default: /backups/ +seed-deploy backup /tmp/my-backup.tgz # custom path +``` + +**Restore** extracts a backup archive, optionally lets you edit the +configuration via the wizard, restores cron jobs, and runs a full deploy. + +```sh +seed-deploy restore /path/to/backup.tar.gz +``` + +## Edge Cases Handled + +- **glibc < 2.25** β€” `deploy.sh` checks glibc version before attempting + Bun install. Prints supported OS versions and exits cleanly. +- **SELinux** β€” all Docker bind mounts use `:z` flag. Without it, + Fedora/CentOS/RHEL silently block container access to host files. +- **Legacy installations** β€” detects old `website_deployment.sh` containers + (`docker run`-based). Stops and removes them before first + `docker compose up` to avoid name conflicts. +- **Non-root operation** β€” containers run as the host user. Caddy binds + ports 80/443 via `CAP_NET_BIND_SERVICE` file capability. `sudo` is + only used when creating directories outside the user's home. +- **Disk exhaustion** β€” old Docker images are pruned both inline after + deploys and on a 4-hour cron schedule. +- **No-change deploys** β€” skipped entirely when compose SHA matches and + all containers are healthy. Shows a hint for `--reconfigure`. +- **Self-update** β€” in headless mode, the script fetches its own latest + version before deploying. Takes effect on the next run. +- **rsync non-fatal** β€” the daemon's monitoring config export uses + `rsync -rlt` (no owner/group) and wraps failures as warnings to avoid + blocking the daemon start. + +## Environment Variables + +For testing and development: + +| Variable | Purpose | +| ----------------- | ------------------------------------------------------------- | +| `SEED_DIR` | Override the seed directory (default: dirname of `deploy.js`) | +| `SEED_DEPLOY_URL` | Override the base URL for fetching compose + deploy.js | +| `SEED_BRANCH` | GitHub branch for `deploy.sh` to fetch from (default: `main`) | + +Example local testing setup: + +```sh +# Terminal 1: serve files locally +cd ops && python3 -m http.server 9999 + +# Terminal 2: run deploy against local server +SEED_DIR=/tmp/seed-test SEED_DEPLOY_URL=http://localhost:9999 sh ops/deploy.sh +``` + +## Development + +```sh +cd ops +bun install # install dependencies +bun test # run test suite +bun run build # bundle to dist/deploy.js +``` + +The `dist/deploy.js` bundle is committed to the repo. A CI workflow +verifies the bundle matches the source on every push to `ops/`.