diff --git a/README.md b/README.md index d1f9fb1..96a0304 100644 --- a/README.md +++ b/README.md @@ -17,35 +17,20 @@ [![MCP](https://img.shields.io/badge/MCP-compatible-8A2BE2)](https://modelcontextprotocol.io) [![Docs](https://img.shields.io/badge/Docs-docs%2F-informational)](docs/) -**Stop giving your AI agents your API keys.** One command. All your tools. Credentials stay out of your agent's hands. +**Build what your agent can do.** - +Define tools, compose workflows, test and run them. +MCP servers, REST APIs, and CLI commands in one config, one UI, one audit log. -Factorly is a local runtime for agent tool chains. It proxies agent tool calls, injects credentials from an encrypted vault, enforces governance rules, and logs everything. + -REST APIs, CLI commands, and MCP servers run through one config, one audit log, one set of rules. +Factorly is a local runtime for agent tool chains. It manages tool calls, injects credentials from an encrypted vault, enforces governance rules, and logs everything. Your agent sees workflows, tools, and data. Secrets stay secret. -Your agent sees workflows, tool names, and data. Never secrets. - -``` -┌────────────┐ ┌────────────┐ ┌────────────┐ -│ │ │ │ │ │ -│ Your Agent │──────▶│ Factorly │──────▶│ Your Tools │ -│ │ │ │ │ │ -└────────────┘ └────────────┘ └────────────┘ - Sees: Vault REST APIs - - tool names Governance CLI commands - - workflows Audit log MCP servers - - data Rate limits - - Never sees: - - API keys - - tokens - - credentials -``` +![Screenshot of Factorly UI](docs/images/screenshot.png) --- + ## Install Install with your package manager of choice: @@ -69,126 +54,58 @@ factorly vault set GITHUB_TOKEN ghp_xxxxxxxxxxxx # 3. Connect to your agent (auto-detects Claude Code, Cursor, Codex) factorly sync -``` - -The agent never sees your credentials. Factorly injected it, made the API call, logged it, and returned the data. - ---- - ---- - -## Features - -MCP servers, REST APIs, CLI commands. One config, one endpoint, one audit log. - -Your agent connects to Factorly once and sees all its approved tools. - -### Encrypted vault - -Your API keys, OAuth tokens, and secrets live in Factorly's fully encrypted local vault, using AES-256-GCM with per-entry encryption. Keys stay on your device. The agent sees tool names and data — never secrets. - -```bash -# Store a secret — encrypted on disk, decrypted on demand -$ factorly vault set GITHUB_TOKEN - Enter value: •••••••••••••••• -# Reference it in any tool config - token: "{{vault:GITHUB_TOKEN}}" - -# Your agent calls a tool — Factorly injects the secret - Agent sees: data - Agent never sees: ghp_xxxx... +# 3. Optional, start the UI +factorly ui ``` -### 36 templates - -Pre-built configs for GitHub, Slack, Stripe, Linear, Gmail, Notion, Jira, HubSpot, Salesforce, and more. One command installs. One command connects to Claude Code, Cursor, or Codex. - -```bash -$ factorly tools import templates github -$ factorly vault set GITHUB_TOKEN ghp_xxxxxxxxxxxx -$ factorly sync -``` +Your agent connects to Factorly as a single MCP server or CLI and sees every tool you've configured. Credentials never leave the vault. -### Wrap any MCP server - -Already using an MCP server? Wrap it with zero config: - -```bash -$ factorly wrap -- npx @modelcontextprotocol/server-github -``` - -Same tools, same interface. Now every call is logged, output is compressed, loops are detected, and calls are rate-limited. - -### Workflows - -Chain tools into governed pipelines. Steps run sequentially with variable passing, conditional branching, and state persistence. One call replaces many. - -```yaml -tools: - deploy.staging: - type: workflow - steps: - - tool: make.test - store: output - - tool: make.deploy - require: "contains(output, 'PASS')" - - switch: - - condition: "contains(output, 'PASS')" - tool: slack.post - params: { text: "✓ Deployed" } - - condition: "true" - tool: slack.post - params: { text: "✗ Deploy blocked — tests failed" } -``` - -`if:` skips steps. `require:` stops the workflow. `switch:` branches. Full [expression language](docs/expressions.md) with comparisons, boolean logic, `contains()`, and `jsonpath()`. Every step is individually logged and governed. +--- -### Oversight +## What It Does -Block destructive operations. Require confirmation before writes. Rate-limit calls. Loop detection is always on — Factorly fingerprints identical calls and blocks runaway agents after 12 repeats. +**Define** — one config, every protocol, [36 templates](docs/templates.md) included -```yaml -shadow: - deny: [delete_repository, delete_branch] - confirm: [merge_pull_request, create_release] - rate_limit: 100/hour -``` +**Test** — Try tools in the [UI](docs/ui.md), see the response, iterate before giving your agent access -Built-in tools block dangerous patterns like `rm -rf`, `curl | sh`, and `DROP TABLE` out of the box. Write and delete templates ship with `confirm: true` by default. +**Compose** — [workflows](docs/workflows.md) with per-step policies, deterministic sequences -```bash -# Agent tries to run a destructive command -$ factorly call shell --command "rm -rf /" - ✗ blocked: command matches deny pattern "rm -rf" +**Govern** — [vault](docs/vault.md), [policies](docs/examples/10-deny-dangerous-operations.md), [audit log](docs/logging.md). Built in, not bolted on. -# Logged and denied. The command never executed. ``` +┌────────────┐ ┌────────────┐ ┌────────────┐ +│ │ │ │ │ │ +│ Your Agent │──────▶│ Factorly │──────▶│ Your Tools │ +│ │ │ │ │ │ +└────────────┘ └────────────┘ └────────────┘ + Sees: Vault REST APIs + - tool names Governance CLI commands + - workflows Audit log MCP servers + - data Rate limits -### Audit trail - -Every tool call logged: who called what, when, with what parameters, what was returned, what was blocked. Per-agent identity tracking for multi-agent setups. - -```bash -$ factorly logs --tool github --status blocked -$ factorly logs -f # follow in real time + Never sees: + - API keys + - tokens + - credentials ``` -### Output compression - -Agent tools return too much data. Factorly compresses JSON, deduplicates log output, filters command-specific noise, and truncates to head + tail — saving tokens without losing signal. 27 built-in filters for common commands (git, make, npm, go, cargo, pytest, pip, docker, kubectl, terraform, and more) apply automatically. Savings tracked per-call in the audit log. +--- ## Docs -- [Installation](docs/getting-started.md) +- [Getting Started](docs/getting-started.md) - [Configuration](docs/config-reference.md) - [CLI Reference](docs/cli-reference.md) +- [Web UI](docs/ui.md) - [Workflows](docs/workflows.md) - [Expressions](docs/expressions.md) - [Output Filters](docs/filters.md) -- [OAuth Setup](docs/oauth.md) -- [Audit Logging](docs/logging.md) -- [Template Library](docs/templates.md) +- [Templates](docs/templates.md) +- [Vault](docs/vault.md) +- [OAuth](docs/oauth.md) +- [Logging](docs/logging.md) +- [OpenAPI Import](docs/openapi-import.md) - [Examples](docs/examples/) ## License diff --git a/docs/README.md b/docs/README.md index 6782099..fa01b19 100644 --- a/docs/README.md +++ b/docs/README.md @@ -31,6 +31,12 @@ | [Config Reference](config-reference.md) | Full YAML schema, auth types, parameters, stdin, interactive, shadow | | [CLI Reference](cli-reference.md) | All commands, subcommands, flags, and environment variables | +## UI + +| Topic | Description | +|-------|-------------| +| [Web UI](ui.md) | Built-in browser interface for browsing, testing, and managing tools | + ## Features | Topic | Description | diff --git a/docs/cli-reference.md b/docs/cli-reference.md index 16d2c48..671ebfd 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -4,7 +4,7 @@ ```bash factorly serve # start MCP server (stdio) -factorly serve --http :3000 # start MCP server (HTTP at /mcp) +factorly serve --port 3000 # start MCP server (HTTP at /mcp) factorly serve --http-token # HTTP with Bearer token auth factorly serve --http-token '{{vault:HTTP_TOKEN}}' # token from vault factorly init # create .factorly/factorly.yaml (interactive) @@ -257,7 +257,8 @@ factorly wrap --url http://localhost:3001/mcp --rate-limit # rate limit (e.g. "100/hour") --max-output # max output bytes per call (default: 50000) --compress # compression hints: "json", "logs", or "all" (default: "all") ---http # serve wrapped server over HTTP (e.g. ":3000") +--host # address to bind (default: 127.0.0.1) +--port # port for HTTP mode --http-token # Bearer token for HTTP mode --env-isolation # "strict" for minimal env, default inherits parent --env KEY=VALUE # set env var (repeatable; supports {{env:VAR}} and {{vault:KEY}}) @@ -285,7 +286,7 @@ Factorly ships with governed alternatives to common agent tools. These are avail ### Context-aware In **stdio mode** (default): all 5 tools available. -In **HTTP mode** (`--http`): only `factorly.fetch` — local tools don't make sense on a remote server. +In **HTTP mode** (`--port`): only `factorly.fetch` — local tools don't make sense on a remote server. ### Safety guards @@ -331,7 +332,7 @@ disable_builtins: true ## HTTP server authentication -When running `factorly serve --http`, you can secure the endpoint with a Bearer token. The token is resolved in order: +When running `factorly serve --port`, you can secure the endpoint with a Bearer token. The token is resolved in order: 1. `--http-token` flag 2. `FACTORLY_HTTP_TOKEN` environment variable @@ -340,25 +341,25 @@ Both support `{{vault:KEY}}` references — the vault is opened automatically if ```bash # Plain token (visible in ps output — use for dev only) -factorly serve --http :3000 --http-token mytoken +factorly serve --port 3000 --http-token mytoken # Environment variable (better for CI/deployment) -FACTORLY_HTTP_TOKEN=mytoken factorly serve --http :3000 +FACTORLY_HTTP_TOKEN=mytoken factorly serve --port 3000 # Vault reference (best — token encrypted at rest) factorly vault set HTTP_TOKEN "my-secret-token" -factorly serve --http :3000 --http-token '{{vault:HTTP_TOKEN}}' +factorly serve --port 3000 --http-token '{{vault:HTTP_TOKEN}}' # Vault ref via env var -FACTORLY_HTTP_TOKEN='{{vault:HTTP_TOKEN}}' factorly serve --http :3000 +FACTORLY_HTTP_TOKEN='{{vault:HTTP_TOKEN}}' factorly serve --port 3000 ``` When a token is set, all HTTP requests must include `Authorization: Bearer `. Requests without a valid token receive a 401 response. A warning is printed to stderr when HTTP mode starts without any token configured. -**Note:** If Factorly is running inside a container (Docker, devcontainer, Codespace), use the host-accessible address — not `localhost`. For Docker, this is typically `host.docker.internal`: +By default, the server binds to `127.0.0.1` (localhost only). If Factorly is running inside a container (Docker, devcontainer, Codespace), bind to all interfaces with `--host 0.0.0.0`: ```bash -factorly serve --http 0.0.0.0:3000 --http-token '{{vault:HTTP_TOKEN}}' +factorly serve --host 0.0.0.0 --port 3000 --http-token '{{vault:HTTP_TOKEN}}' factorly sync --http host.docker.internal:3000 --token '{{vault:HTTP_TOKEN}}' ``` diff --git a/docs/config-reference.md b/docs/config-reference.md index d2e9097..404cd1f 100644 --- a/docs/config-reference.md +++ b/docs/config-reference.md @@ -7,6 +7,9 @@ tools_dir: ./tools # optional, scan directory for tool files disabled_commands: [vault, exec] # optional, block specific CLI commands disable_builtins: true # optional, disable all factorly.* built-in tools +disabled_builtins: # optional, disable specific built-in tools + - factorly.shell + - factorly.clipboard vault_backends: # optional, external secret managers op: # backend name → use as {{op:KEY}} @@ -29,7 +32,7 @@ oauth_providers: # shared across tools tools: : - type: cli | rest | mcp | workflow # required + type: cli | rest | mcp | workflow | builtin # required (builtin is internal-only) description: "..." # optional, shown to agent # For CLI commands: @@ -85,7 +88,7 @@ tools: required: true - name: limit in: query - type: integer # string (default), integer, number, boolean, json + type: integer # string (default), text, integer, number, boolean, json default: 100 # default value if not provided by caller min: 1 # clamp to minimum (integers and numbers) max: 100 # clamp to maximum (integers and numbers) @@ -142,6 +145,39 @@ tools: ``` +## Parameter types + +| Type | Description | +|------|-------------| +| `string` | Single-line text (default) | +| `text` | Multi-line text — same as string at the protocol level, but renders as a textarea in the UI | +| `integer` | Whole number — supports `min`/`max` clamping | +| `number` | Decimal number — supports `min`/`max` clamping | +| `boolean` | `true` or `false` | +| `json` | Arbitrary JSON value | + +The `text` type is useful for parameters that accept long-form content like prompts, code snippets, or message bodies. It behaves identically to `string` in the MCP schema and execution — the difference is only in UI rendering (multiline textarea vs single-line input). + +```yaml +tools: + summarize: + type: rest + base_url: https://api.openai.com/v1 + method: POST + path: /chat/completions + auth: + type: bearer + token: "{{vault:OPENAI_KEY}}" + parameters: + - name: content + type: text # renders as textarea in the UI + required: true + description: "Text to summarize" + - name: model + type: string + default: "gpt-4o" +``` + ## Secret references Use `{{env:ENV_VAR}}` for environment variables or `{{vault:KEY}}` for encrypted vault secrets. Both are resolved at startup before the agent sees anything. @@ -192,7 +228,7 @@ The `type` field controls how body parameters are serialized in JSON: | Type | JSON output | Example | |------|------------|---------| -| `string` (default) | Quoted | `"claude-sonnet-4-20250514"` | +| `string` (default) | Quoted | `"claude-sonnet-4-6"` | | `integer` | Unquoted number | `1024` | | `number` | Unquoted number | `0.95` | | `boolean` | Unquoted | `true` | @@ -260,7 +296,7 @@ tools: description: The question for Claude required: true - name: model - default: claude-sonnet-4-20250514 + default: claude-sonnet-4-6 - name: max_tokens default: "1024" ``` diff --git a/docs/examples/11-require-confirmation.md b/docs/examples/11-require-confirmation.md index c4a42ce..c105f63 100644 --- a/docs/examples/11-require-confirmation.md +++ b/docs/examples/11-require-confirmation.md @@ -58,8 +58,19 @@ factorly call email --to "team@acme.com" --subject "Deploy complete" 1. The agent calls a tool that matches a confirm rule. 2. **CLI mode** (`factorly call`): Factorly prompts on stderr — `⚠ Tool "x" requires confirmation. Proceed? (y/n)`. The call only executes if you type `y`. 3. **MCP mode** (`factorly serve`): Factorly uses MCP elicitation. The AI client (Claude Code, Cursor) displays the confirmation prompt in the chat UI. -4. If denied, the agent receives an error and the rejection is logged. -5. `confirm: true` guards every call on that tool. `confirm: [action1, action2]` only guards the listed sub-tools. +4. **UI mode** (`factorly ui`): Confirmation prompts appear as a modal in the browser. If an MCP agent is also connected, both the browser and the agent see the prompt — **first response wins**. +5. If no response is received within **60 seconds**, the call is automatically **denied**. +6. If denied, the agent receives an error and the rejection is logged. +7. `confirm: true` guards every call on that tool. `confirm: [action1, action2]` only guards the listed sub-tools. + +### Verbose logging + +With `--verbose`, Factorly logs confirmation routing: + +``` +[confirm] factorly.shell: waiting for approval (browser, elicitation) +[confirm] factorly.shell: approved via browser +``` --- diff --git a/docs/examples/25-mcp-http-remote.md b/docs/examples/25-mcp-http-remote.md index c5a4193..39791e3 100644 --- a/docs/examples/25-mcp-http-remote.md +++ b/docs/examples/25-mcp-http-remote.md @@ -6,11 +6,11 @@ Serve Factorly's tools over HTTP so remote agents (or agents inside containers) ```bash # Start the HTTP server with token auth -factorly serve --http :3000 --http-token mytoken +factorly serve --port 3000 --http-token mytoken # Or use a vault-stored token (recommended) factorly vault set HTTP_TOKEN my-secret-token -factorly serve --http :3000 --http-token '{{vault:HTTP_TOKEN}}' +factorly serve --port 3000 --http-token '{{vault:HTTP_TOKEN}}' ``` ## Config @@ -32,11 +32,11 @@ Connect Claude Code to the HTTP server by adding to `.mcp.json`: ### Docker setup -When Factorly runs inside a container, use `host.docker.internal` from the host or bind to `0.0.0.0`: +When Factorly runs inside a container, bind to all interfaces with `--host 0.0.0.0`: ```bash # Inside the container -factorly serve --http 0.0.0.0:3000 --http-token '{{vault:HTTP_TOKEN}}' +factorly serve --host 0.0.0.0 --port 3000 --http-token '{{vault:HTTP_TOKEN}}' ``` ```json @@ -60,7 +60,7 @@ factorly sync --http host.docker.internal:3000 --token '{{vault:HTTP_TOKEN}}' ## What happens -1. `factorly serve --http :3000` starts an HTTP server at `/mcp` using the MCP Streamable HTTP transport. +1. `factorly serve --port 3000` starts an HTTP server at `/mcp` using the MCP Streamable HTTP transport. By default it binds to `127.0.0.1` (localhost only) — use `--host 0.0.0.0` for containers. 2. `--http-token` enables Bearer token authentication. Every request must include `Authorization: Bearer ` or it receives a 401. 3. All configured tools are exposed over HTTP. Built-in tools (`factorly.shell`, `factorly.write_file`, etc.) are hidden in HTTP mode since local operations don't make sense on a remote server — only `factorly.fetch` remains available. 4. Use HTTP mode when: the agent runs in a different process/container, you need multiple agents sharing one server, or stdio isn't available. Use stdio mode (the default) for local, single-agent setups — it's simpler and has no auth overhead. diff --git a/docs/examples/26-built-in-tools.md b/docs/examples/26-built-in-tools.md index f8c754b..135aef2 100644 --- a/docs/examples/26-built-in-tools.md +++ b/docs/examples/26-built-in-tools.md @@ -1,16 +1,16 @@ # Built-in Tools -Factorly ships five governed tools that work out of the box — no YAML needed. All are prefixed `factorly.` to avoid collisions. +Factorly ships five governed tools that work out of the box — no YAML needed. All are prefixed `factorly.` to avoid collisions. They execute **in-process** using Go's standard library (no subprocesses for read/write/fetch). ## Available tools -| Tool | Description | -|------|-------------| -| `factorly.shell` | Run a shell command (confirm required, destructive patterns blocked) | -| `factorly.read_file` | Read a local file (sensitive paths blocked) | -| `factorly.write_file` | Write a local file (confirm required, system paths blocked) | -| `factorly.fetch` | HTTP GET a URL (cloud metadata and private networks blocked) | -| `factorly.clipboard` | Copy text to clipboard (confirm required) | +| Tool | Description | Implementation | +|------|-------------|----------------| +| `factorly.shell` | Run a shell command (confirm required, destructive patterns blocked) | `exec.Command` with 30s timeout | +| `factorly.read_file` | Read a local file (sensitive paths blocked) | `os.ReadFile`, project-scoped | +| `factorly.write_file` | Write a local file (confirm required, system paths blocked) | `os.WriteFile`, project-scoped | +| `factorly.fetch` | HTTP GET a URL (cloud metadata and private networks blocked) | `net/http`, 1MB limit | +| `factorly.clipboard` | Copy text to clipboard (confirm required) | Platform-aware (pbcopy/xclip/xsel) | ## Usage @@ -28,11 +28,21 @@ factorly tools github.repos rest GET /users/{username}/repos ``` +## Project scoping + +File operations (`read_file`, `write_file`) are **scoped to the project directory** (the directory containing your `factorly.yaml`). Any attempt to read or write outside this directory is rejected: + +``` +path "/etc/passwd" is outside project directory "/home/user/myproject" +``` + +Relative paths are resolved against the project root. Path traversal (`../../../etc/passwd`) is blocked. + ## Safety guards Built-in tools block dangerous operations by default: -- **Shell**: blocks `rm -rf /`, `curl | sh`, `DROP TABLE`, `shutdown`, fork bombs +- **Shell**: blocks `rm -rf /`, `curl | sh`, `DROP TABLE`, `shutdown`, fork bombs. Uses `sh -c` on Unix, `cmd /C` on Windows. - **Read/Write**: blocks `.env`, `.ssh/id_*`, `*.pem`, `credentials.json`, `/etc/shadow`, system directories - **Fetch**: blocks cloud metadata (`169.254.169.254`), localhost, private networks, `file://` protocol @@ -58,22 +68,25 @@ tools: Disable specific built-ins or all of them: -```bash -# Disable specific tools via env var -export FACTORLY_DISABLED_TOOLS=factorly.shell,factorly.write_file -``` - ```yaml -# Or disable all built-ins in config +# Disable specific built-in tools +disabled_builtins: + - factorly.shell + - factorly.clipboard + +# Or disable all built-ins disable_builtins: true ``` ## What happens 1. Built-in tools are registered automatically when Factorly starts. They appear in `factorly tools` alongside your configured tools. -2. Each built-in has default oversight rules (confirm prompts, blocked patterns). These apply without any config. -3. You can loosen restrictions using `allow_patterns`, `allow_paths`, or `allow_urls` in a `shadow` block — but only for specific values, not blanket overrides. -4. In HTTP mode (`factorly serve --http`), local tools (`shell`, `read_file`, `write_file`, `clipboard`) are hidden. Only `factorly.fetch` is available since local filesystem operations don't apply to remote servers. +2. Each built-in executes in-process (no subprocess), except `clipboard` which uses the platform clipboard command. +3. File operations are scoped to the project directory. Shell commands run from the working directory. +4. Each built-in has default oversight rules (confirm prompts, blocked patterns). These apply without any config. +5. You can loosen restrictions using `allow_patterns`, `allow_paths`, or `allow_urls` in a `shadow` block — but only for specific values, not blanket overrides. +6. In HTTP mode (`factorly serve --port`), local tools (`shell`, `read_file`, `write_file`, `clipboard`) are hidden. Only `factorly.fetch` is available since local filesystem operations don't apply to remote servers. +7. In the UI (`factorly ui`), built-in tools appear with a "Built-in" badge and locked configuration — only oversight settings are editable. --- diff --git a/docs/examples/33-workflow-expressions.md b/docs/examples/33-workflow-expressions.md new file mode 100644 index 0000000..2098cb6 --- /dev/null +++ b/docs/examples/33-workflow-expressions.md @@ -0,0 +1,121 @@ +# Workflow Expressions + +Use `{{expr:...}}` in step params to transform data between steps — extract JSON fields, format timestamps, manipulate strings. + +## Config + +```yaml +# .factorly/factorly.yaml +tools: + daily.summary: + type: workflow + description: Fetch today's calendar events and post a summary to Slack + parameters: + - name: channel + default: "#general" + steps: + - tool: google-calendar.list_events + params: + calendarId: primary + timeMin: "{{expr:now()}}" + timeMax: "{{expr:now('24h')}}" + maxResults: "10" + store: events + + - tool: slack.post_message + if: "jsonpath(events, '$.items') != ''" + params: + channel: "{{channel}}" + text: "{{expr:concat('You have ', len(events), ' events today')}}" +``` + +## Run it + +```bash +factorly call daily.summary +factorly call daily.summary --channel '#engineering' +``` + +## More examples + +### Extract and transform JSON + +```yaml +steps: + - tool: api.get_user + params: + id: "{{user_id}}" + store: user + + - tool: notification.send + params: + # Extract nested JSON field + email: "{{expr:jsonpath(user, '$.contact.email')}}" + # Uppercase the name + greeting: "{{expr:concat('Hello ', upper(jsonpath(user, '$.name')))}}" + # Default if missing + role: "{{expr:default(jsonpath(user, '$.role'), 'member')}}" +``` + +### Date-based API calls + +```yaml +steps: + - tool: analytics.query + params: + start_date: "{{expr:today('-7')}}" + end_date: "{{expr:today()}}" + store: metrics + + - tool: report.generate + params: + title: "{{expr:concat('Weekly Report: ', today('-7'), ' to ', today())}}" + data: "{{metrics}}" +``` + +### String manipulation + +```yaml +steps: + - tool: git.log + store: log_output + + - tool: slack.post + params: + # First 500 chars of output + text: "{{expr:left(log_output, 500)}}" + # Extract filename from path + filename: "{{expr:cut('/home/user/docs/report.pdf', '/', '-1')}}" + # Clean up whitespace + summary: "{{expr:trim(log_output)}}" +``` + +### Conditional with expressions + +```yaml +steps: + - tool: api.health_check + store: health + + - tool: pagerduty.alert + # Only alert if status is not healthy + if: "jsonpath(health, '$.status') != 'healthy'" + params: + severity: "critical" + message: "{{expr:concat('Service down: ', jsonpath(health, '$.message'))}}" +``` + +## Available functions + +See the full [expressions reference](../expressions.md#value-functions) for all 20 functions: `concat`, `cut`, `default`, `find`, `join`, `jsonpath`, `left`, `len`, `lower`, `now`, `replace`, `right`, `substr`, `today`, `trim`, `upper`, and more. + +## How it works + +1. `{{expr:...}}` is a resolver backend — same system as `{{vault:KEY}}` and `{{env:VAR}}` +2. In workflows, expressions have access to stored step outputs and input params +3. In any tool call, stateless functions like `now()`, `today()`, `upper()` work +4. Expressions that fail return empty string — no workflow crashes + +--- + +[← Back to Examples](README.md) diff --git a/docs/examples/README.md b/docs/examples/README.md index f99cc09..08a4cb7 100644 --- a/docs/examples/README.md +++ b/docs/examples/README.md @@ -72,6 +72,7 @@ Practical, copy-paste examples for every Factorly feature. Each is self-containe | 30 | [Workflows](30-workflows.md) | Chain tools into governed pipelines with variable passing | | 31 | [Workflow Conditionals](31-workflow-conditionals.md) | Branch with `if:` and `switch:` using expressions and jsonpath | | 32 | [Changelog Workflow](32-changelog-workflow.md) | Gather git log, diff stats, and contributors in one call | +| 33 | [Workflow Expressions](33-workflow-expressions.md) | Transform data between steps with `now()`, `jsonpath()`, `concat()`, and more | ## Compliance diff --git a/docs/expressions.md b/docs/expressions.md index 619e94e..70d14e3 100644 --- a/docs/expressions.md +++ b/docs/expressions.md @@ -243,14 +243,148 @@ Use `condition: "true"` as a default/else case. Each switch case supports `tool`, `params`, and `store` (same as a regular step). If no condition matches, the switch step is skipped. +## Value expressions + +Use `{{expr:...}}` in parameter values to evaluate functions inline. Works in **any tool call** (CLI, REST, MCP, workflows) — not just workflow steps. + +```yaml +tools: + my.api: + type: rest + base_url: https://api.example.com + method: GET + path: /events + parameters: + - name: since + default: "{{expr:now('-24h')}}" +``` + +The `{{expr:...}}` pattern is a resolver backend — it works alongside `{{vault:KEY}}` and `{{env:VAR}}` in the same string, all resolved in one pass. + +### In workflow step params + +Expressions are most powerful in workflows where they can reference stored step outputs: + +```yaml +steps: + - tool: calendar.list_events + params: + timeMin: "{{expr:now()}}" + timeMax: "{{expr:now('24h')}}" + store: events + + - tool: slack.post + params: + message: "Next event: {{expr:jsonpath(events, '$.items[0].summary')}}" + count: "{{expr:len(events)}}" +``` + +Mix `{{expr:...}}` with simple `{{variable}}` substitution and `{{vault:KEY}}` references in the same string: + +```yaml +params: + message: "Hello {{name}}, you have {{expr:len(items)}} items" + auth: "{{vault:API_KEY}}" +``` + +### Value functions + +| Function | Description | Example | +|----------|-------------|---------| +| `concat(a, b, ...)` | Concatenate values | `concat(first, ' ', last)` → `John Doe` | +| `cut(val, delim)` | Split by delimiter → JSON array | `cut(csv, ',')` → `["a","b"]` | +| `cut(val, delim, index)` | Split and pick field | `cut(path, '/', '-1')` → last segment | +| `default(var, fallback)` | Value or fallback if empty | `default(title, 'Untitled')` | +| `find(val, needle)` | Index of substring (-1 if not found) | `find(path, '/')` → `4` | +| `join(val, sep)` | Join JSON array | `join(tags, ', ')` → `go, rust` | +| `jsonpath(var, path)` | Extract from JSON | `jsonpath(data, '$.items[0].name')` | +| `left(val, n)` | First n characters | `left(id, 8)` → `abcdefgh` | +| `len(val)` | Length (array, object, or string) | `len(items)` → `3` | +| `lower(val)` | Lowercase | `lower(name)` → `jordan` | +| `now()` | Current UTC timestamp (RFC3339) | `now()` → `2026-05-08T12:00:00Z` | +| `now(offset)` | Timestamp with offset | `now('24h')`, `now('-1h')`, `now('30m')` | +| `replace(val, old, new)` | String replacement | `replace(output, '\n', ' ')` | +| `right(val, n)` | Last n characters | `right(hash, 7)` → `a1b2c3d` | +| `substr(val, start)` | Substring from position | `substr(id, 0, 8)` | +| `substr(val, start, end)` | Substring with end | `substr(hash, 0, 7)` | +| `today()` | Today's date (YYYY-MM-DD) | `today()` → `2026-05-09` | +| `today(offset)` | Date with offset | `today('7')` → 7 days from now | +| `trim(val)` | Strip whitespace | `trim(output)` | +| `upper(val)` | Uppercase | `upper(name)` → `JORDAN` | + +These functions also work in `if:`/`require:` conditions and `switch:` cases — they return values that can be compared: + +```yaml +if: "len(items) > 0" +if: "jsonpath(data, '$.status') == 'ok'" +if: "lower(env) == 'prod'" +``` + +### `cut()` behavior + +Splits a string by delimiter. Without an index, returns a JSON array. With an index, returns the specific field. Supports negative indexing (`'-1'` for last). + +```yaml +# path = "a/b/c/d" +cut(path, '/') # → ["a","b","c","d"] +cut(path, '/', 0) # → "a" +cut(path, '/', '-1') # → "d" + +# csv = "name,age,city" +cut(csv, ',', 1) # → "age" +``` + +### `join()` behavior + +Parses the value as a JSON array and joins elements with the separator. Non-array values pass through unchanged. + +```yaml +# tags = ["go", "rust", "python"] +join(tags, ', ') # → "go, rust, python" +join(tags, ' | ') # → "go | rust | python" +``` + +### `len()` behavior + +| Input | Result | +|-------|--------| +| JSON array `["a","b"]` | `2` | +| JSON object `{"a":1}` | `1` | +| Plain string `"hello"` | `5` | +| Empty `""` | `0` | + +### `now()` offsets + +The offset uses Go duration syntax: `h` (hours), `m` (minutes), `s` (seconds). Prefix with `-` for past. + +| Offset | Result | +|--------|--------| +| `now()` | Current time | +| `now('24h')` | 24 hours from now | +| `now('-1h')` | 1 hour ago | +| `now('30m')` | 30 minutes from now | +| `now('1h30m')` | 90 minutes from now | + +### `today()` offsets + +Returns the date in `YYYY-MM-DD` format. Accepts a duration or day count as offset. + +| Offset | Result | +|--------|--------| +| `today()` | Today | +| `today('7')` | 7 days from now | +| `today('-1')` | Yesterday | +| `today('48h')` | 2 days from now (duration syntax) | + ## Error handling -Expressions that fail to parse or evaluate return false (fail-closed). This means: +Expressions that fail to parse or evaluate return false (fail-closed) in conditions, or empty string in value expressions. This means: -- Typos in variable names → falsy (step skipped) -- Malformed expressions → falsy -- Invalid JSON in jsonpath → falsy -- Division by zero, overflow → falsy +- Typos in variable names → falsy / empty +- Malformed expressions → falsy / empty +- Invalid JSON in jsonpath → falsy / empty +- Division by zero, overflow → falsy / empty +- Invalid `now()` offset → current time (no offset applied) No panics, no workflow crashes from bad expressions. diff --git a/docs/getting-started.md b/docs/getting-started.md index 06eac60..403bf1e 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -112,10 +112,10 @@ This writes the Factorly MCP server entry into `.mcp.json` (Claude Code), `.curs For remote or shared servers: ```bash -factorly serve --http :3000 +factorly serve --port 3000 ``` -Endpoint at `http://localhost:3000/mcp`. Secure with `--http-token` or `FACTORLY_HTTP_TOKEN`. See [CLI Reference](cli-reference.md) for details. +Endpoint at `http://localhost:3000/mcp`. Secure with `--http-token` or `FACTORLY_HTTP_TOKEN`. See [CLI Reference](cli-reference.md) for details. Use `--host 0.0.0.0` to bind to all interfaces (e.g. for containers). > **Note:** If running inside Docker, use `host.docker.internal` instead of `localhost`. See [CLI Reference](cli-reference.md#http-server-authentication) for container setup. diff --git a/docs/images/screenshot-templates.png b/docs/images/screenshot-templates.png new file mode 100644 index 0000000..830b1dc Binary files /dev/null and b/docs/images/screenshot-templates.png differ diff --git a/docs/images/screenshot.png b/docs/images/screenshot.png new file mode 100644 index 0000000..26bb88d Binary files /dev/null and b/docs/images/screenshot.png differ diff --git a/docs/ui.md b/docs/ui.md new file mode 100644 index 0000000..e0604ab --- /dev/null +++ b/docs/ui.md @@ -0,0 +1,59 @@ +# Web UI + +Factorly includes a built-in web interface for browsing, testing, and managing your tools. + +```bash +factorly ui +``` + +![Screenshot of Factorly UI](images/screenshot-templates.png) + +## What you can do + +- **Browse tools** — see all configured tools grouped by prefix, with type badges (CLI, REST, MCP, Built-in, Workflow) +- **Try It** — execute any tool from the browser with a form for parameters, view JSON responses with collapsible tree, copy output +- **Edit tools** — modify configuration, parameters, oversight, and output filters inline +- **Workflows** — visual step editor with conditional logic, variable passing, and per-step tool selection +- **Confirm prompts** — approve or deny shadow confirm prompts from the browser (works alongside MCP elicitation) +- **Vault** — manage encrypted credentials without touching the CLI +- **Auth** — configure and refresh OAuth providers +- **Activity feed** — live SSE stream of all tool calls as they happen +- **Import** — import tools from OpenAPI specs or templates + +## Options + +```bash +factorly ui [flags] + --port int Port to serve on (default 4100) + --host string Host to bind to (default "127.0.0.1") + --no-launch Don't open the browser automatically + --mcp Enable MCP endpoint on the same server + --mcp-token Bearer token for MCP endpoint authentication +``` + +## Security + +- **Per-run nonce token** — a random token is generated on each launch, stored in an HttpOnly cookie (SameSite=Strict) +- **Host validation** — only accepts requests from localhost/127.0.0.1 +- **Local development only** — a warning is shown if bound to a non-localhost address +- **Confirm timeout** — unresponded prompts auto-deny after 60 seconds + +## Confirm prompts + +When a tool has `shadow.confirm: true`, the UI shows a modal with tool name and parameters. You can approve or deny from the browser. + +If an MCP agent is also connected, both the browser and the agent see the prompt — first response wins. + +Prompts are delivered via SSE with a polling fallback (every 2s) for reliability. + +## MCP co-hosting + +With `--mcp`, the UI server also exposes an MCP endpoint at `/mcp`. Your agent connects to one URL for both the tool API and the browser interface. + +```bash +factorly ui --mcp --mcp-token "{{vault:MCP_TOKEN}}" +``` + +--- + +[← Back to Docs](README.md) diff --git a/docs/vault.md b/docs/vault.md index 31a99f9..f4ee650 100644 --- a/docs/vault.md +++ b/docs/vault.md @@ -33,10 +33,24 @@ token: "{{env:GITHUB_TOKEN}}" # Vault (encrypted on disk, decrypted on demand) token: "{{vault:GITHUB_TOKEN}}" + +# With a default fallback +base_url: "{{vault:API_URL|https://api.example.com}}" ``` Both work. Mix and match per tool. Vault references are resolved at call time — the agent never sees either form. +All `{{prefix:content}}` references use the same resolver pattern. Available backends: + +| Prefix | Source | Example | +|--------|--------|---------| +| `vault` | Encrypted local vault | `{{vault:GITHUB_TOKEN}}` | +| `env` | Environment variable | `{{env:HOME}}` | +| `op` | 1Password (external backend) | `{{op:ABCDEF}}` | +| `expr` | [Expression function](expressions.md#value-expressions) | `{{expr:now()}}` | + +All can be mixed in a single string: `{{vault:TOKEN}} expires {{expr:now('24h')}}`. + ## How it works ``` diff --git a/docs/workflows.md b/docs/workflows.md index 23fc4ba..4ae7b3f 100644 --- a/docs/workflows.md +++ b/docs/workflows.md @@ -35,7 +35,7 @@ factorly call daily.summary 1. Workflow receives input parameters from the caller 2. Steps execute sequentially, each through the full proxy pipeline (parameter validation → shadow policy → execute → output filter → log) 3. `store:` saves step output as a named variable for later steps -4. `{{var}}` in step params is substituted from stored outputs + input params +4. `{{var}}` in step params is substituted from stored outputs + input params. Use `{{expr:func(...)}}` for [value expressions](expressions.md#value-expressions-in-step-params). 5. On failure, remaining steps are skipped 6. State is persisted to `.factorly/workflows/.json` after each step 7. Output includes the full execution trace as JSON @@ -47,7 +47,7 @@ factorly call daily.summary | Field | Required | Description | |-------|----------|-------------| | `tool` | Yes* | Tool name to call (must exist in config) | -| `params` | No | Parameters to pass — supports `{{var}}` substitution | +| `params` | No | Parameters to pass — supports `{{var}}` and `{{expr:...}}` substitution | | `store` | No | Save step output as a named variable | | `if` | No | [Expression](expressions.md) — skip step when false, continue workflow | | `require` | No | [Expression](expressions.md) — stop workflow when false | diff --git a/src/cmd/factorly/auth_cmd.go b/src/cmd/factorly/auth_cmd.go index 475a25c..f6c085b 100644 --- a/src/cmd/factorly/auth_cmd.go +++ b/src/cmd/factorly/auth_cmd.go @@ -42,7 +42,7 @@ var authLoginCmd = &cobra.Command{ } // Open vault to resolve refs in client_id/secret and to store tokens - backend, err := getCachedLocalVault() + backend, err := getCachedVault() if err != nil { return err } @@ -95,7 +95,7 @@ var authStatusCmd = &cobra.Command{ return err } - backend, err := getCachedLocalVault() + backend, err := getCachedVault() if err != nil { return err } @@ -158,7 +158,7 @@ var authLogoutCmd = &cobra.Command{ return err } - backend, err := getCachedLocalVault() + backend, err := getCachedVault() if err != nil { return err } diff --git a/src/cmd/factorly/main.go b/src/cmd/factorly/main.go index 18b45cd..df1ff89 100644 --- a/src/cmd/factorly/main.go +++ b/src/cmd/factorly/main.go @@ -185,7 +185,7 @@ func runToolsList(cmd *cobra.Command, args []string) error { w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) fmt.Fprintln(w, "NAME\tTYPE\tDESCRIPTION\tPARAMETERS") - for _, t := range reg.List() { + for _, t := range reg.ListVisible() { if shadowPolicy != nil && shadowPolicy.IsDenied(t.Name) { continue } @@ -480,7 +480,7 @@ func init() { toolsCmd.AddCommand(toolsListCmd, addCmd, removeCmd, importCmd, recordCmd, statusCmd) utilsCmd.AddCommand(autocompleteCmd) - rootCmd.AddCommand(versionCmd, toolsCmd, callCmd, initCmd, syncCmd, vaultCmd, authCmd, serveCmd, wrapCmd, execCmd, logsCmd, utilsCmd) + rootCmd.AddCommand(versionCmd, toolsCmd, callCmd, initCmd, syncCmd, vaultCmd, authCmd, serveCmd, wrapCmd, execCmd, logsCmd, utilsCmd, uiCmd) } // loadConfig loads config and builds a registry. Does not open the vault @@ -503,6 +503,7 @@ func loadConfig() (*config.Config, *registry.Registry, error) { cfgPath := configPath if cfgPath == "" { cfgPath = config.FindConfig() + configPath = cfgPath // persist for UI/other commands that need the write path vlog("found config: %s", cfgPath) } else { vlog("using config: %s", cfgPath) @@ -544,6 +545,7 @@ func loadConfig() (*config.Config, *registry.Registry, error) { Name: name, Type: toolCfg.Type, Description: toolCfg.Description, + Hidden: toolCfg.Hidden, Parameters: params, ProviderKey: toolCfg.Type, MaxOutput: toolCfg.MaxOutput, @@ -566,6 +568,13 @@ func loadConfig() (*config.Config, *registry.Registry, error) { return cfg, reg, nil } +var cachedResolver *vault.Resolver + +// getCachedResolver returns the resolver from the last bootstrapProviders call. +func getCachedResolver() *vault.Resolver { + return cachedResolver +} + // bootstrapProviders opens the vault if needed, creates providers, and // wires everything into a proxy. Takes config and registry from loadConfig(). // confirmFn is used for shadow confirm prompts — nil uses the default CLI prompt. @@ -575,6 +584,14 @@ func bootstrapProviders(cfg *config.Config, reg *registry.Registry, confirmFn .. if err != nil { return nil, err } + cachedResolver = resolver + + // Register expr as a resolver backend so {{expr:now()}} etc. work everywhere + if resolver != nil { + resolver.RegisterFunc("expr", func(content string) (string, error) { + return provider.EvalExpr(content, nil), nil + }) + } providers := make(map[string]provider.Provider) cliTools := make(map[string]provider.CLIToolDef) @@ -611,12 +628,25 @@ func bootstrapProviders(cfg *config.Config, reg *registry.Registry, confirmFn .. cliTools[name] = def vlog(" registered cli tool: %s", name) case "rest": + var vaultRefs []string + collectRef := func(original string) { + if vault.HasVaultRefs(original) { + vaultRefs = append(vaultRefs, original) + } + } + collectRef(toolCfg.BaseURL) + collectRef(toolCfg.Path) + if toolCfg.Auth != nil { + collectRef(toolCfg.Auth.Token) + collectRef(toolCfg.Auth.Value) + } restDef := provider.RESTToolDef{ - Method: toolCfg.Method, - BaseURL: resolveVaultRefTracked(resolver, toolCfg.BaseURL, &vaultKeys), - Path: toolCfg.Path, - Body: toolCfg.Body, - Headers: resolveVaultMap(resolver, toolCfg.Headers), + Method: toolCfg.Method, + BaseURL: resolveVaultRefTracked(resolver, toolCfg.BaseURL, &vaultKeys), + Path: resolveVaultRefTracked(resolver, toolCfg.Path, &vaultKeys), + Body: toolCfg.Body, + BodyType: toolCfg.BodyType, + Headers: resolveVaultMap(resolver, toolCfg.Headers), } if toolCfg.Auth != nil { authDef := &provider.AuthDef{ @@ -627,6 +657,8 @@ func bootstrapProviders(cfg *config.Config, reg *registry.Registry, confirmFn .. } if toolCfg.Auth.Type == "oauth" { oauthCfg := cfg.ResolveOAuthProvider(toolCfg.Auth) + collectRef(oauthCfg.ClientID) + collectRef(oauthCfg.ClientSecret) authDef.OAuthProvider = &oauth.ProviderConfig{ ClientID: resolveVaultRefTracked(resolver, oauthCfg.ClientID, &vaultKeys), ClientSecret: resolveVaultRefTracked(resolver, oauthCfg.ClientSecret, &vaultKeys), @@ -654,6 +686,13 @@ func bootstrapProviders(cfg *config.Config, reg *registry.Registry, confirmFn .. vlog("warning: invalid timeout %q for rest tool %s: %v", toolCfg.Timeout, name, err) } } + if len(vaultRefs) > 0 && resolver != nil { + refs := vaultRefs // capture for closure + res := resolver // capture for closure + restDef.RedactSecrets = func(s string) string { + return res.Redact(s, refs) + } + } restTools[name] = restDef vlog(" registered rest tool: %s", name) case "mcp": @@ -673,6 +712,8 @@ func bootstrapProviders(cfg *config.Config, reg *registry.Registry, confirmFn .. } mcpServers[name] = def vlog(" registered mcp server: %s", name) + case "builtin": + vlog(" registered builtin tool: %s", name) case "workflow": steps := make([]provider.WorkflowStep, len(toolCfg.Steps)) for i, s := range toolCfg.Steps { @@ -710,6 +751,15 @@ func bootstrapProviders(cfg *config.Config, reg *registry.Registry, confirmFn .. return nil, err } + // Built-in provider (in-process, no subprocess) + // Scope file operations to the project directory (config file's parent) + projectDir := "" + if configPath != "" { + projectDir = filepath.Dir(configPath) + } + providers["builtin"] = provider.NewBuiltinProvider(serveMode, projectDir) + vlog("initialized builtin provider (root: %s)", projectDir) + if len(cliTools) > 0 { vlog("initialized cli provider (%d tools)", len(cliTools)) providers["cli"] = provider.NewCLI(cliTools) @@ -811,6 +861,10 @@ func bootstrapProviders(cfg *config.Config, reg *registry.Registry, confirmFn .. vlog("shadow policy active (%d rules)", len(shadowRules)) } + if resolver != nil { + proxyOpts = append(proxyOpts, proxy.WithResolver(resolver)) + } + p := proxy.New(reg, providers, logIface, proxyOpts...) // Wire workflow provider after proxy creation (needs proxy reference for step execution) @@ -927,27 +981,18 @@ func initResolver(cfg *config.Config) (*vault.Resolver, error) { vlog("registered external vault backend: %s", name) } - // Try to open local vault - // resolveVaultPath() respects --vault-path, --global, FACTORLY_VAULT_PATH, - // project vault, and global vault — in that priority order. - resolvedPath := resolveVaultPath() - if _, err := os.Stat(resolvedPath); err == nil { - var backend vault.Backend - var openErr error - - backend, openErr = getCachedVault() - if openErr != nil { - if len(cfg.VaultBackends) > 0 { - vlog("local vault failed to open: %v (external backends available)", openErr) - } else { - return nil, fmt.Errorf("vault required but failed to open: %w", openErr) - } + // Try to open vault (project vault, global vault, or fallback) + backend, openErr := getCachedVault() + if openErr != nil { + if len(cfg.VaultBackends) > 0 { + vlog("local vault failed to open: %v (external backends available)", openErr) } else { - resolver.Register("vault", backend) - vlog("vault opened successfully") + vlog("warning: vault references found but vault unavailable: %v — refs will not be resolved", openErr) + return resolver, nil } - } else if len(cfg.VaultBackends) == 0 { - return nil, fmt.Errorf("vault required but no vault found at %s (run 'factorly vault set' to create one)", resolvedPath) + } else { + resolver.Register("vault", backend) + vlog("vault opened successfully") } return resolver, nil diff --git a/src/cmd/factorly/serve_cmd.go b/src/cmd/factorly/serve_cmd.go index 9ff93c6..2149662 100644 --- a/src/cmd/factorly/serve_cmd.go +++ b/src/cmd/factorly/serve_cmd.go @@ -20,8 +20,12 @@ import ( "github.com/spf13/cobra" ) -var httpAddr string -var httpToken string +var ( + httpHost string + httpPort int + httpAddr string // legacy --http flag + httpToken string +) var serveCmd = &cobra.Command{ Use: "serve", @@ -29,12 +33,20 @@ var serveCmd = &cobra.Command{ Long: `Start an MCP server that exposes all configured Factorly tools to MCP clients like Claude Code and Cursor. -By default, uses stdio transport. Use --http to start an HTTP server instead.`, +By default, uses stdio transport. Use --port to start an HTTP server instead.`, RunE: func(cmd *cobra.Command, args []string) error { if err := checkCommandAllowed("serve"); err != nil { return err } + // --host/--port or legacy --http both enable HTTP mode + if httpPort != 0 && httpAddr == "" { + httpAddr = fmt.Sprintf("%s:%d", httpHost, httpPort) + } if httpAddr != "" { + // Default to localhost when only a port is given (e.g. ":3000") + if strings.HasPrefix(httpAddr, ":") { + httpAddr = "127.0.0.1" + httpAddr + } serveMode = "http" } cfg, reg, err := loadConfig() @@ -181,8 +193,10 @@ func mcpElicitConfirm(ctx context.Context, toolName string, params map[string]st } func init() { - serveCmd.Flags().StringVar(&httpAddr, "http", "", - "start HTTP transport on this address (e.g. :3000) instead of stdio") + serveCmd.Flags().StringVar(&httpHost, "host", "127.0.0.1", "address to bind the HTTP server (use 0.0.0.0 for all interfaces)") + serveCmd.Flags().IntVar(&httpPort, "port", 0, "port for HTTP transport (enables HTTP mode)") + serveCmd.Flags().StringVar(&httpAddr, "http", "", "start HTTP transport on this address (legacy; prefer --host/--port)") serveCmd.Flags().StringVar(&httpToken, "http-token", "", "require Bearer token authentication for HTTP transport") + _ = serveCmd.Flags().MarkHidden("http") } diff --git a/src/cmd/factorly/ui_cmd.go b/src/cmd/factorly/ui_cmd.go new file mode 100644 index 0000000..d274a4f --- /dev/null +++ b/src/cmd/factorly/ui_cmd.go @@ -0,0 +1,327 @@ +// Copyright 2026 Jordan Sherer +// SPDX-License-Identifier: gpl + +package main + +import ( + "context" + "crypto/rand" + "encoding/hex" + "fmt" + "net" + "net/http" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "time" + + "github.com/factorly-dev/factorly/internal/proxy" + factorlyServer "github.com/factorly-dev/factorly/internal/server" + "github.com/factorly-dev/factorly/internal/shadow" + "github.com/factorly-dev/factorly/internal/ui" + "github.com/factorly-dev/factorly/internal/vault" + mcpserver "github.com/mark3labs/mcp-go/server" + "github.com/spf13/cobra" +) + +var ( + uiHost string + uiPort int + uiMCP bool + uiMCPToken string + uiNoLaunch bool +) + +var uiCmd = &cobra.Command{ + Use: "ui", + Short: "Open the Factorly web UI", + Long: "Starts a localhost web server with a visual interface for configuring tools, running them, and managing workflows.", + RunE: runUI, +} + +func init() { + uiCmd.Flags().StringVar(&uiHost, "host", "127.0.0.1", "address to bind the UI server (use 0.0.0.0 for all interfaces)") + uiCmd.Flags().IntVar(&uiPort, "port", 3741, "port for the UI server") + uiCmd.Flags().BoolVar(&uiMCP, "mcp", false, "also serve MCP endpoint at /mcp") + uiCmd.Flags().StringVar(&uiMCPToken, "mcp-token", "", "bearer token for MCP endpoint (required when --mcp is set)") + uiCmd.Flags().BoolVar(&uiNoLaunch, "no-launch", false, "don't open the browser automatically") +} + +func runUI(cmd *cobra.Command, args []string) error { + cfg, reg, err := loadConfig() + if err != nil { + return err + } + + // Set up confirm broker for routing shadow confirm prompts to the browser + confirmBroker := ui.NewConfirmBroker() + confirmFn := shadow.ConfirmFunc(confirmBroker.Request) + if uiMCP { + // Race MCP elicitation and browser confirm — first response wins. + // This ensures prompts always show in the browser UI AND the agent + // can respond via elicitation, whichever is faster. + mcpFn := shadow.ConfirmFunc(mcpElicitConfirm) + confirmFn = func(ctx context.Context, toolName string, params map[string]string) bool { + // Timeout: deny if no response within 1 minute + ctx, cancel := context.WithTimeout(ctx, 60*time.Second) + defer cancel() + + // Always send to browser + browserCtx, browserCancel := context.WithCancel(ctx) + defer browserCancel() + + type confirmResult struct { + approved bool + source string + } + result := make(chan confirmResult, 2) + + channels := []string{"browser"} + go func() { + result <- confirmResult{confirmBroker.Request(browserCtx, toolName, params), "browser"} + }() + + // If there's an MCP session, also try elicitation + if mcpserver.ClientSessionFromContext(ctx) != nil { + channels = append(channels, "elicitation") + go func() { + result <- confirmResult{mcpFn(ctx, toolName, params), "elicitation"} + }() + } + + vlog("[confirm] %s: waiting for approval (%s)", toolName, strings.Join(channels, ", ")) + + // First response wins + select { + case r := <-result: + action := "approved" + if !r.approved { + action = "denied" + } + vlog("[confirm] %s: %s via %s", toolName, action, r.source) + return r.approved + case <-ctx.Done(): + vlog("[confirm] %s: denied (timed out after 60s)", toolName) + return false + } + } + } + p, err := bootstrapProviders(cfg, reg, confirmFn) + if err != nil { + return err + } + defer p.Teardown() + + // Get vault backend (cached singleton from bootstrapProviders, won't re-prompt) + var vaultBackend vault.Backend + vaultBackend, _ = getCachedVault() + + // Detect project vs global vault from the cached backend + var projectVault, globalVault vault.Backend + projectPath := filepath.Join(".factorly", "vault.enc") + globalPath := vault.DefaultVaultPath() + if fb, ok := vaultBackend.(*vault.FallbackBackend); ok { + projectVault = fb.Primary + globalVault = fb.Secondary + } else if vaultBackend != nil { + // Single vault — determine which one it is + if _, err := os.Stat(projectPath); err == nil { + projectVault = vaultBackend + } else if _, err := os.Stat(globalPath); err == nil { + globalVault = vaultBackend + } else { + projectVault = vaultBackend + } + } + + // Set up activity broadcaster for live feed + activity := ui.NewActivityBroadcaster() + p.SetOnCall(func(e proxy.CallEvent) { + activity.Broadcast(e) + }) + + srv, err := ui.New(ui.Options{ + Config: cfg, + CfgPath: configPath, + ToolsDir: resolveToolsDir(configPath, cfg.ToolsDir), + Registry: reg, + Proxy: p, + Vault: vaultBackend, + Resolver: getCachedResolver(), + ProjectVault: projectVault, + GlobalVault: globalVault, + Activity: activity, + ConfirmBroker: confirmBroker, + }) + if err != nil { + return err + } + + // Generate per-run nonce token + token := generateNonce() + var uiWarnings []string + + // Optionally mount MCP endpoint on the UI server's mux + if uiMCP { + mcpSrv := factorlyServer.New(reg, p) + mcpHTTP := mcpserver.NewStreamableHTTPServer(mcpSrv) + + // Resolve MCP token: flag → env var → vault refs + if uiMCPToken == "" { + uiMCPToken = os.Getenv("FACTORLY_HTTP_TOKEN") + } + if uiMCPToken != "" && vault.HasVaultRefs(uiMCPToken) { + if backend, vErr := getCachedLocalVault(); vErr == nil { + resolver := vault.NewResolver() + resolver.Register("vault", backend) + uiMCPToken = resolveVaultRef(resolver, uiMCPToken) + } + } + if uiMCPToken == "" { + uiWarnings = append(uiWarnings, "MCP endpoint has no authentication. Use --mcp-token or FACTORLY_HTTP_TOKEN.") + } + + // Wrap MCP handler with bearer token auth + var mcpHandler http.Handler = mcpHTTP + if uiMCPToken != "" { + mcpHandler = tokenAuthMiddleware(mcpHTTP, uiMCPToken) + } + srv.MountMCP(mcpHandler) + } + + // Wrap with host validation + token check + handler := hostValidation(tokenValidation(srv.Handler(), token)) + + addr := fmt.Sprintf("%s:%d", uiHost, uiPort) + url := fmt.Sprintf("http://localhost:%d/?token=%s", uiPort, token) + + if uiHost != "127.0.0.1" && uiHost != "localhost" { + uiWarnings = append(uiWarnings, "The UI is bound to "+uiHost+" — intended for local development only. Do not expose to the internet.") + } + if len(uiWarnings) > 0 { + fmt.Fprintf(cmd.ErrOrStderr(), "⚠ WARNING: %s\n", strings.Join(uiWarnings, " ")) + } + fmt.Fprintf(cmd.ErrOrStderr(), "Factorly UI running at %s\n", url) + if uiMCP { + fmt.Fprintf(cmd.ErrOrStderr(), "MCP endpoint at http://localhost:%d/mcp (token: %s)\n", uiPort, uiMCPToken) + } + + // Start listener first, then open browser to avoid race condition + // where the browser connects before the server is ready + ln, err := net.Listen("tcp", addr) + if err != nil { + return err + } + + if !uiNoLaunch { + go openBrowser(url) + } + + return http.Serve(ln, handler) +} + +// hostValidation rejects requests with unexpected Host headers. +func hostValidation(next http.Handler) http.Handler { + allowed := map[string]bool{ + "localhost": true, + "127.0.0.1": true, + "host.docker.internal": true, + } + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + host := r.Host + // Strip port + if i := strings.LastIndex(host, ":"); i != -1 { + host = host[:i] + } + if !allowed[host] { + http.Error(w, "forbidden: invalid host", http.StatusForbidden) + return + } + next.ServeHTTP(w, r) + }) +} + +// tokenValidation requires a valid token on the first request (sets a cookie), +// then validates the cookie on subsequent requests. Static assets are exempt. +func tokenValidation(next http.Handler, token string) http.Handler { + const cookieName = "factorly_session" + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Static assets and MCP endpoint don't need cookie auth + if strings.HasPrefix(r.URL.Path, "/static/") || strings.HasPrefix(r.URL.Path, "/mcp") { + next.ServeHTTP(w, r) + return + } + + // Check cookie first + if cookie, err := r.Cookie(cookieName); err == nil && cookie.Value == token { + next.ServeHTTP(w, r) + return + } + + // Check token query param (initial page load from browser open) + if r.URL.Query().Get("token") == token { + // Set session cookie and redirect to clean URL + http.SetCookie(w, &http.Cookie{ + Name: cookieName, + Value: token, + Path: "/", + HttpOnly: true, + Secure: r.TLS != nil, + SameSite: http.SameSiteStrictMode, + }) + // Redirect to strip token from URL (local path only, no open redirect) + redirectPath := r.URL.EscapedPath() + if redirectPath == "" { + redirectPath = "/" + } + q := r.URL.Query() + q.Del("token") + if encoded := q.Encode(); encoded != "" { + redirectPath += "?" + encoded + } + http.Redirect(w, r, redirectPath, http.StatusFound) + return + } + + http.Error(w, "unauthorized", http.StatusUnauthorized) + }) +} + +func resolveToolsDir(cfgPath, toolsDir string) string { + configDir := filepath.Dir(cfgPath) + if toolsDir != "" { + if filepath.IsAbs(toolsDir) { + return toolsDir + } + return filepath.Join(configDir, toolsDir) + } + // Auto-discover .factorly/tools/ convention + candidate := filepath.Join(configDir, "tools") + if info, err := os.Stat(candidate); err == nil && info.IsDir() { + return candidate + } + return "" +} + +func generateNonce() string { + b := make([]byte, 16) + _, _ = rand.Read(b) + return hex.EncodeToString(b) +} + +func openBrowser(url string) { + var cmd *exec.Cmd + switch runtime.GOOS { + case "darwin": + cmd = exec.Command("open", url) + case "linux": + cmd = exec.Command("xdg-open", url) + case "windows": + cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", url) + } + if cmd != nil { + _ = cmd.Start() + } +} diff --git a/src/internal/builtins/builtins.go b/src/internal/builtins/builtins.go index 85fbae6..c85ec28 100644 --- a/src/internal/builtins/builtins.go +++ b/src/internal/builtins/builtins.go @@ -4,9 +4,6 @@ package builtins import ( - "os/exec" - "runtime" - "github.com/factorly-dev/factorly/internal/config" ) @@ -15,9 +12,8 @@ type Options struct { Mode string // "stdio" or "http" — local tools only register in stdio mode } -// Register adds built-in tools to the config. Built-ins are overseen -// alternatives to unsafe agent tools: shell, read, write, fetch, clipboard. -// They use the existing CLI provider — no new provider needed. +// Register adds built-in tools to the config. Built-ins execute in-process +// via the builtin provider — no subprocess needed for most operations. func Register(cfg *config.Config, opts Options) { if cfg.DisableBuiltins { return @@ -26,87 +22,71 @@ func Register(cfg *config.Config, opts Options) { cfg.Tools = make(map[string]config.ToolConfig) } + disabled := make(map[string]bool, len(cfg.DisabledBuiltins)) + for _, name := range cfg.DisabledBuiltins { + disabled[name] = true + } + + register := func(name string, tc config.ToolConfig) { + if disabled[name] { + return + } + cfg.Tools[name] = tc + } + // Universal (all modes) — runs server-side where credentials live - cfg.Tools["factorly.fetch"] = config.ToolConfig{ - Type: "cli", + register("factorly.fetch", config.ToolConfig{ + Type: "builtin", Description: "Fetch a URL (overseen, logged, compressed)", - Command: "curl", - Args: []string{"-sS", "{{url}}"}, Compress: []string{"all"}, MaxOutput: 50000, Parameters: []config.ParamConfig{ {Name: "url", Description: "URL to fetch", Required: true}, }, - } + }) // Local only (stdio mode) — runs on the agent's machine if opts.Mode == "http" { return } - cfg.Tools["factorly.shell"] = config.ToolConfig{ - Type: "cli", + register("factorly.shell", config.ToolConfig{ + Type: "builtin", Description: "Run a shell command (overseen, logged, compressed)", - Command: "sh", - Args: []string{"-c", "{{command}}"}, Compress: []string{"all"}, MaxOutput: 50000, Shadow: &config.ShadowConfig{Confirm: true}, Parameters: []config.ParamConfig{ {Name: "command", Description: "Shell command to execute", Required: true}, }, - } + }) - cfg.Tools["factorly.read_file"] = config.ToolConfig{ - Type: "cli", + register("factorly.read_file", config.ToolConfig{ + Type: "builtin", Description: "Read a local file (overseen, logged, compressed)", - Command: "cat", - Args: []string{"{{path}}"}, Compress: []string{"all"}, MaxOutput: 50000, Parameters: []config.ParamConfig{ {Name: "path", Description: "File path to read", Required: true}, }, - } + }) - cfg.Tools["factorly.write_file"] = config.ToolConfig{ - Type: "cli", + register("factorly.write_file", config.ToolConfig{ + Type: "builtin", Description: "Write content to a local file (overseen, logged, confirmable)", - Command: "tee", - Args: []string{"{{path}}"}, - Stdin: "{{content}}", Shadow: &config.ShadowConfig{Confirm: true}, Parameters: []config.ParamConfig{ {Name: "path", Description: "File path to write", Required: true}, {Name: "content", Description: "Content to write", Required: true}, }, - } + }) - cmd, args := clipboardCommand() - cfg.Tools["factorly.clipboard"] = config.ToolConfig{ - Type: "cli", + register("factorly.clipboard", config.ToolConfig{ + Type: "builtin", Description: "Copy text to the system clipboard (overseen, logged, confirmable)", - Command: cmd, - Args: args, - Stdin: "{{text}}", Shadow: &config.ShadowConfig{Confirm: true}, Parameters: []config.ParamConfig{ - {Name: "text", Description: "Text to copy to clipboard", Required: true}, + {Name: "text", Type: "text", Description: "Text to copy to clipboard", Required: true}, }, - } -} - -// clipboardCommand returns the platform-specific clipboard command. -func clipboardCommand() (string, []string) { - switch runtime.GOOS { - case "darwin": - return "pbcopy", nil - case "windows": - return "clip", nil - default: - if _, err := exec.LookPath("xsel"); err == nil { - return "xsel", []string{"--clipboard", "--input"} - } - return "xclip", []string{"-selection", "clipboard"} - } + }) } diff --git a/src/internal/builtins/builtins_test.go b/src/internal/builtins/builtins_test.go index 6ceaf82..a7c6800 100644 --- a/src/internal/builtins/builtins_test.go +++ b/src/internal/builtins/builtins_test.go @@ -58,8 +58,67 @@ func TestRegisterOverwritesUserTools(t *testing.T) { } Register(cfg, Options{Mode: "stdio"}) - if cfg.Tools["factorly.shell"].Command == "user-shell" { - t.Error("expected built-in to overwrite user tool") + if cfg.Tools["factorly.shell"].Type != "builtin" { + t.Error("expected built-in to overwrite user tool with type builtin") + } +} + +func TestRegisterTypeIsBuiltin(t *testing.T) { + cfg := &config.Config{Tools: make(map[string]config.ToolConfig)} + Register(cfg, Options{Mode: "stdio"}) + + for name, tc := range cfg.Tools { + if tc.Type != "builtin" { + t.Errorf("%s: got type %q, want \"builtin\"", name, tc.Type) + } + } +} + +func TestRegisterDisabledBuiltins(t *testing.T) { + cfg := &config.Config{ + Tools: make(map[string]config.ToolConfig), + DisabledBuiltins: []string{"factorly.shell", "factorly.clipboard"}, + } + Register(cfg, Options{Mode: "stdio"}) + + if _, ok := cfg.Tools["factorly.shell"]; ok { + t.Error("factorly.shell should be disabled") + } + if _, ok := cfg.Tools["factorly.clipboard"]; ok { + t.Error("factorly.clipboard should be disabled") + } + if _, ok := cfg.Tools["factorly.read_file"]; !ok { + t.Error("factorly.read_file should still be registered") + } + if _, ok := cfg.Tools["factorly.fetch"]; !ok { + t.Error("factorly.fetch should still be registered") + } +} + +func TestRegisterShadowPreserved(t *testing.T) { + cfg := &config.Config{Tools: make(map[string]config.ToolConfig)} + Register(cfg, Options{Mode: "stdio"}) + + // Shell and write_file should have confirm by default + shellShadow := cfg.Tools["factorly.shell"].Shadow + if shellShadow == nil { + t.Fatal("factorly.shell should have shadow config") + } + if _, all := shellShadow.ConfirmList(); !all { + t.Error("factorly.shell should have confirm=true") + } + + writeShadow := cfg.Tools["factorly.write_file"].Shadow + if writeShadow == nil { + t.Fatal("factorly.write_file should have shadow config") + } + if _, all := writeShadow.ConfirmList(); !all { + t.Error("factorly.write_file should have confirm=true") + } + + // read_file should NOT have shadow + if cfg.Tools["factorly.read_file"].Shadow != nil { + t.Error("factorly.read_file should not have shadow config") } } diff --git a/src/internal/config/config.go b/src/internal/config/config.go index 015acce..53aaa61 100644 --- a/src/internal/config/config.go +++ b/src/internal/config/config.go @@ -32,6 +32,7 @@ type Config struct { VaultBackends map[string]vault.ExternalBackendConfig `yaml:"vault_backends,omitempty"` DisabledCommands []string `yaml:"disabled_commands,omitempty"` DisableBuiltins bool `yaml:"disable_builtins,omitempty"` + DisabledBuiltins []string `yaml:"disabled_builtins,omitempty"` } type OAuthProviderConfig struct { @@ -45,6 +46,7 @@ type OAuthProviderConfig struct { type ToolConfig struct { Type string `yaml:"type"` Description string `yaml:"description"` + Hidden bool `yaml:"hidden,omitempty"` // hidden from MCP listing and tools list, but callable directly and by workflows Command string `yaml:"command,omitempty"` Args []string `yaml:"args,omitempty"` Stdin string `yaml:"stdin,omitempty"` @@ -60,12 +62,13 @@ type ToolConfig struct { URL string `yaml:"url,omitempty"` // MCP HTTP transport URL // REST fields - BaseURL string `yaml:"base_url,omitempty"` - Method string `yaml:"method,omitempty"` - Path string `yaml:"path,omitempty"` - Body string `yaml:"body,omitempty"` // JSON body template with {{param}} placeholders - Headers map[string]string `yaml:"headers,omitempty"` - Auth *AuthConfig `yaml:"auth,omitempty"` + BaseURL string `yaml:"base_url,omitempty"` + Method string `yaml:"method,omitempty"` + Path string `yaml:"path,omitempty"` + Body string `yaml:"body,omitempty"` // JSON body template with {{param}} placeholders + BodyType string `yaml:"body_type,omitempty"` // "json" (default), "form", "raw" + Headers map[string]string `yaml:"headers,omitempty"` + Auth *AuthConfig `yaml:"auth,omitempty"` // Workflow fields Steps []StepConfig `yaml:"steps,omitempty"` @@ -221,19 +224,26 @@ func Load(path string) (*Config, error) { return nil, err } - // Always scan .factorly/tools/ if it exists (convention for templates and modular configs) + // Scan .factorly/tools/ if it exists, unless already loaded via tools_dir toolsSubDir := filepath.Join(configDir, "tools") - if info, err := os.Stat(toolsSubDir); err == nil && info.IsDir() { - vlog("loading tool files from %s", toolsSubDir) - subDirTools, err := loadDir(toolsSubDir) - if err != nil { - return nil, err - } - if len(subDirTools) > 0 { - vlog(" found %d tools in %s", len(subDirTools), toolsSubDir) - } - if err := mergeTools(cfg.Tools, subDirTools); err != nil { - return nil, err + resolvedToolsDir := cfg.ToolsDir + if resolvedToolsDir != "" && !filepath.IsAbs(resolvedToolsDir) { + resolvedToolsDir = filepath.Join(configDir, resolvedToolsDir) + } + alreadyLoaded := resolvedToolsDir != "" && filepath.Clean(resolvedToolsDir) == filepath.Clean(toolsSubDir) + if !alreadyLoaded { + if info, err := os.Stat(toolsSubDir); err == nil && info.IsDir() { + vlog("loading tool files from %s", toolsSubDir) + subDirTools, err := loadDir(toolsSubDir) + if err != nil { + return nil, err + } + if len(subDirTools) > 0 { + vlog(" found %d tools in %s", len(subDirTools), toolsSubDir) + } + if err := mergeTools(cfg.Tools, subDirTools); err != nil { + return nil, err + } } } } else { @@ -506,9 +516,6 @@ func validate(cfg *Config) error { } } if tool.Type == "workflow" { - if len(tool.Steps) == 0 { - return fmt.Errorf("config: workflow tool %q has no steps", name) - } for i, step := range tool.Steps { if step.Tool == "" && len(step.Switch) == 0 { return fmt.Errorf("config: workflow tool %q step %d missing tool name", name, i+1) diff --git a/src/internal/config/config_test.go b/src/internal/config/config_test.go index 04e58a9..ed9143c 100644 --- a/src/internal/config/config_test.go +++ b/src/internal/config/config_test.go @@ -880,8 +880,8 @@ tools: description: Empty workflow `) _, err := Load(path) - if err == nil { - t.Fatal("expected error for workflow with no steps") + if err != nil { + t.Fatalf("workflow with no steps should be valid, got: %v", err) } } diff --git a/src/internal/openapi/openapi.go b/src/internal/openapi/openapi.go index bdf534f..63cb0ba 100644 --- a/src/internal/openapi/openapi.go +++ b/src/internal/openapi/openapi.go @@ -7,8 +7,11 @@ import ( "encoding/json" "fmt" "io" + "net" "net/http" + "net/url" "os" + "path/filepath" "regexp" "strings" @@ -17,12 +20,13 @@ import ( ) type GenerateOpts struct { - Prefix string // tool name prefix, defaults to slugified info.title + Prefix string // tool name prefix, defaults to slugified info.title + BaseDir string // if set, local file paths are restricted to this directory } // Generate reads an OpenAPI 3.x spec (local file or URL) and returns Factorly tool definitions. func Generate(specPath string, opts GenerateOpts) (map[string]config.ToolConfig, error) { - data, err := readSpec(specPath) + data, err := readSpec(specPath, opts.BaseDir) if err != nil { return nil, err } @@ -71,6 +75,13 @@ func Generate(specPath string, opts GenerateOpts) (map[string]config.ToolConfig, description := extractDescription(op) params := extractParameters(op) + // Merge path-level parameters (shared across methods) + pathLevelParams := extractParameters(methods) + params = mergeParams(pathLevelParams, params) + + // Ensure all path placeholders have a corresponding parameter + params = ensurePathParams(path, params) + tc := config.ToolConfig{ Type: "rest", Description: description, @@ -215,9 +226,58 @@ func extractParameters(op map[string]any) []config.ParamConfig { return params } +// mergeParams merges path-level params with operation-level params. +// Operation-level params take precedence (override by name). +func mergeParams(pathLevel, opLevel []config.ParamConfig) []config.ParamConfig { + if len(pathLevel) == 0 { + return opLevel + } + seen := make(map[string]bool) + for _, p := range opLevel { + seen[p.Name] = true + } + var merged []config.ParamConfig + for _, p := range pathLevel { + if !seen[p.Name] { + merged = append(merged, p) + } + } + return append(merged, opLevel...) +} + +// ensurePathParams checks for {param} placeholders in the path and adds +// any missing parameters as required path params. +func ensurePathParams(path string, params []config.ParamConfig) []config.ParamConfig { + re := regexp.MustCompile(`\{([^}]+)\}`) + matches := re.FindAllStringSubmatch(path, -1) + if len(matches) == 0 { + return params + } + + existing := make(map[string]bool) + for _, p := range params { + existing[p.Name] = true + } + + for _, m := range matches { + name := m[1] + if !existing[name] { + params = append(params, config.ParamConfig{ + Name: name, + Required: true, + In: "path", + }) + } + } + return params +} + func buildToolName(prefix, method, path string, op map[string]any) string { if opID, ok := op["operationId"].(string); ok && opID != "" { - return prefix + "." + opID + // Sanitize: replace / and other non-safe chars with . for dot-separated naming + safe := strings.ReplaceAll(opID, "/", ".") + safe = strings.ReplaceAll(safe, " ", "_") + return prefix + "." + safe } // Fallback: method_path_slug slug := slugify(path) @@ -247,9 +307,19 @@ func isHTTPMethod(m string) bool { return false } -func readSpec(specPath string) ([]byte, error) { +func readSpec(specPath, baseDir string) ([]byte, error) { if strings.HasPrefix(specPath, "http://") || strings.HasPrefix(specPath, "https://") { - resp, err := http.Get(specPath) //nolint:gosec // user-provided URL is intentional + parsed, err := url.Parse(specPath) + if err != nil { + return nil, fmt.Errorf("invalid spec URL: %w", err) + } + if parsed.Scheme != "http" && parsed.Scheme != "https" { + return nil, fmt.Errorf("unsupported URL scheme %q (only http/https allowed)", parsed.Scheme) + } + if err := checkSpecURL(parsed); err != nil { + return nil, err + } + resp, err := http.Get(parsed.String()) //nolint:gosec,noctx // user-provided URL is intentional for spec import if err != nil { return nil, fmt.Errorf("fetching spec from %s: %w", specPath, err) } @@ -257,15 +327,60 @@ func readSpec(specPath string) ([]byte, error) { if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("fetching spec from %s: %s", specPath, resp.Status) } - data, err := io.ReadAll(resp.Body) + // Limit read to 10MB to prevent resource exhaustion + data, err := io.ReadAll(io.LimitReader(resp.Body, 10<<20)) if err != nil { return nil, fmt.Errorf("reading spec from %s: %w", specPath, err) } return data, nil } - data, err := os.ReadFile(specPath) + // File path: resolve to absolute and verify it exists + absPath, err := filepath.Abs(specPath) + if err != nil { + return nil, fmt.Errorf("resolving spec path: %w", err) + } + // When baseDir is set, restrict reads to that directory. + if baseDir != "" { + absBase, err := filepath.Abs(baseDir) + if err != nil { + return nil, fmt.Errorf("resolving base dir: %w", err) + } + // EvalSymlinks resolves symlink traversal before the prefix check. + resolved, err := filepath.EvalSymlinks(absPath) + if err != nil { + return nil, fmt.Errorf("reading spec: %w", err) + } + if !strings.HasPrefix(resolved, absBase+string(filepath.Separator)) && resolved != absBase { + return nil, fmt.Errorf("spec path %q is outside the project directory", specPath) + } + } + data, err := os.ReadFile(absPath) // #nosec G304 -- validated against baseDir above when called from UI if err != nil { return nil, fmt.Errorf("reading spec: %w", err) } return data, nil } + +// checkSpecURL blocks requests to internal/private network targets. +func checkSpecURL(u *url.URL) error { + host := u.Hostname() + + // Block cloud metadata endpoints + if host == "169.254.169.254" || host == "metadata.google.internal" { + return fmt.Errorf("spec URL blocked: cloud metadata endpoint") + } + + // Block localhost/loopback + if host == "localhost" || host == "127.0.0.1" || host == "0.0.0.0" || host == "::1" { + return fmt.Errorf("spec URL blocked: localhost access denied") + } + + // Block private/link-local IPs + if ip := net.ParseIP(host); ip != nil { + if ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast() { + return fmt.Errorf("spec URL blocked: private network access denied") + } + } + + return nil +} diff --git a/src/internal/openapi/openapi_test.go b/src/internal/openapi/openapi_test.go index 50711fd..61debf6 100644 --- a/src/internal/openapi/openapi_test.go +++ b/src/internal/openapi/openapi_test.go @@ -6,6 +6,7 @@ package openapi import ( "os" "path/filepath" + "strings" "testing" ) @@ -264,6 +265,156 @@ func TestGenerateInvalidSpec(t *testing.T) { } } +func TestBuildToolName_SanitizesSlashes(t *testing.T) { + tests := []struct { + prefix string + method string + path string + op map[string]any + want string + }{ + {"github", "GET", "/repos", map[string]any{"operationId": "repos/list-for-user"}, "github.repos.list-for-user"}, + {"github", "GET", "/actions/runs", map[string]any{"operationId": "actions/list-workflow-runs"}, "github.actions.list-workflow-runs"}, + {"api", "POST", "/users", map[string]any{"operationId": "createUser"}, "api.createUser"}, + {"api", "GET", "/pets/{petId}", map[string]any{}, "api.get_pets_petid"}, + } + for _, tt := range tests { + t.Run(tt.want, func(t *testing.T) { + got := buildToolName(tt.prefix, tt.method, tt.path, tt.op) + if got != tt.want { + t.Errorf("buildToolName(%q, %q, %q, ...) = %q, want %q", tt.prefix, tt.method, tt.path, got, tt.want) + } + }) + } +} + +func TestGeneratePathParams(t *testing.T) { + spec := ` +openapi: "3.0.0" +info: + title: Test API + version: "1.0" +paths: + /users/{username}/repos/{repo_id}: + get: + operationId: getUserRepo + description: Get a user repo +` + path := writeSpec(t, spec) + tools, err := Generate(path, GenerateOpts{}) + if err != nil { + t.Fatal(err) + } + + tc, ok := tools["test_api.getUserRepo"] + if !ok { + t.Fatal("tool not found") + } + + // Should have both path params + paramNames := make(map[string]bool) + for _, p := range tc.Parameters { + paramNames[p.Name] = true + if p.Name == "username" || p.Name == "repo_id" { + if !p.Required { + t.Errorf("path param %q should be required", p.Name) + } + } + } + if !paramNames["username"] { + t.Error("missing path param 'username'") + } + if !paramNames["repo_id"] { + t.Error("missing path param 'repo_id'") + } +} + +func TestGeneratePathParamsWithExplicit(t *testing.T) { + spec := ` +openapi: "3.0.0" +info: + title: Test API + version: "1.0" +paths: + /users/{username}: + get: + operationId: getUser + parameters: + - name: username + in: path + required: true + description: The user's login name +` + path := writeSpec(t, spec) + tools, err := Generate(path, GenerateOpts{}) + if err != nil { + t.Fatal(err) + } + + tc := tools["test_api.getUser"] + // Should have exactly one username param (not duplicated) + count := 0 + for _, p := range tc.Parameters { + if p.Name == "username" { + count++ + if p.Description != "The user's login name" { + t.Errorf("description should come from explicit param, got %q", p.Description) + } + } + } + if count != 1 { + t.Errorf("expected 1 username param, got %d", count) + } +} + +func TestReadSpec_RejectsInvalidScheme(t *testing.T) { + _, err := readSpec("ftp://evil.example.com/spec.yaml", "") + // ftp is not http/https, so it falls through to file read which should fail + if err == nil { + t.Fatal("expected error for non-http URL treated as file path") + } +} + +func TestReadSpec_RelativePath(t *testing.T) { + // Create a spec file in a temp dir + dir := t.TempDir() + specContent := `openapi: "3.0.0" +info: + title: Test + version: "1.0" +paths: + /test: + get: + operationId: test_get +` + specPath := filepath.Join(dir, "spec.yaml") + _ = os.WriteFile(specPath, []byte(specContent), 0o644) + + data, err := readSpec(specPath, "") + if err != nil { + t.Fatalf("readSpec failed: %v", err) + } + if !strings.Contains(string(data), "test_get") { + t.Error("spec content not read correctly") + } +} + +func TestReadSpec_TraversalPath(t *testing.T) { + // This should resolve via filepath.Abs and fail because the file doesn't exist + _, err := readSpec("../../../etc/passwd", "") + if err == nil { + t.Fatal("expected error for traversal path") + } +} + +func TestGenerate_HTTPURLValidation(t *testing.T) { + // Non-existent host should fail with network error, not panic + _, err := Generate("https://nonexistent.invalid.example.com/spec.json", GenerateOpts{}) + if err == nil { + t.Fatal("expected error for unreachable URL") + } +} + func TestSlugify(t *testing.T) { tests := []struct { input string diff --git a/src/internal/output/filter_test.go b/src/internal/output/filter_test.go index 8dfcbdd..5870887 100644 --- a/src/internal/output/filter_test.go +++ b/src/internal/output/filter_test.go @@ -526,7 +526,7 @@ func TestJSONPathArrayWildcard(t *testing.T) { } func TestJSONPathAnthropicUseCase(t *testing.T) { - input := `{"id":"msg_123","content":[{"type":"text","text":"Hello world"}],"model":"claude-sonnet-4-20250514","usage":{"input_tokens":10,"output_tokens":5}}` + input := `{"id":"msg_123","content":[{"type":"text","text":"Hello world"}],"model":"claude-sonnet-4-6","usage":{"input_tokens":10,"output_tokens":5}}` f := &Filter{JSONPath: mustParseJP(t, "$.content[0].text")} got := f.Apply(input) if got != "Hello world" { diff --git a/src/internal/provider/builtin.go b/src/internal/provider/builtin.go new file mode 100644 index 0000000..62f6691 --- /dev/null +++ b/src/internal/provider/builtin.go @@ -0,0 +1,227 @@ +// Copyright 2026 Jordan Sherer +// SPDX-License-Identifier: gpl + +package provider + +import ( + "context" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "time" +) + +// BuiltinHandler is a function that executes a built-in tool in-process. +type BuiltinHandler func(params map[string]string) (*Result, error) + +// BuiltinProvider executes built-in tools in-process without forking subprocesses. +type BuiltinProvider struct { + handlers map[string]BuiltinHandler + rootDir string // project root — file operations are scoped to this directory +} + +// NewBuiltinProvider creates a provider with in-process handlers for built-in tools. +// Mode controls which tools are available: "http" excludes local-only tools. +// rootDir scopes file read/write operations to the project directory. +func NewBuiltinProvider(mode string, rootDir string) *BuiltinProvider { + bp := &BuiltinProvider{ + handlers: make(map[string]BuiltinHandler), + rootDir: rootDir, + } + + // Universal — available in all modes + bp.handlers["factorly.fetch"] = builtinFetch + + // Local only — stdio mode + if mode != "http" { + bp.handlers["factorly.read_file"] = bp.builtinReadFile + bp.handlers["factorly.write_file"] = bp.builtinWriteFile + bp.handlers["factorly.shell"] = builtinShell + bp.handlers["factorly.clipboard"] = builtinClipboard + } + + return bp +} + +func (bp *BuiltinProvider) Setup() error { return nil } +func (bp *BuiltinProvider) Teardown() error { return nil } + +func (bp *BuiltinProvider) Execute(toolName string, params map[string]string) (*Result, error) { + h, ok := bp.handlers[toolName] + if !ok { + return nil, fmt.Errorf("builtin: unknown tool %q", toolName) + } + start := time.Now() + result, err := h(params) + if err != nil { + return nil, err + } + result.Duration = time.Since(start) + return result, nil +} + +// scopedPath resolves and validates that a path is within the project root. +// Returns the absolute path or an error result if out of bounds. +func (bp *BuiltinProvider) scopedPath(path string) (string, *Result) { + if path == "" { + return "", &Result{Error: "path is required", ExitCode: 1} + } + // Resolve relative paths against rootDir + abs := path + if bp.rootDir != "" && !filepath.IsAbs(path) { + abs = filepath.Join(bp.rootDir, path) + } + abs = filepath.Clean(abs) + + // Verify the resolved path is within rootDir + if bp.rootDir != "" { + root := filepath.Clean(bp.rootDir) + if !strings.HasPrefix(abs, root+string(filepath.Separator)) && abs != root { + return "", &Result{ + Error: fmt.Sprintf("path %q is outside project directory %q", path, root), + ExitCode: 1, + } + } + } + return abs, nil +} + +// builtinReadFile reads a file using os.ReadFile, scoped to project dir. +func (bp *BuiltinProvider) builtinReadFile(params map[string]string) (*Result, error) { + abs, errResult := bp.scopedPath(params["path"]) + if errResult != nil { + return errResult, nil + } + data, err := os.ReadFile(abs) + if err != nil { + return &Result{Error: err.Error(), ExitCode: 1}, nil + } + return &Result{Output: string(data)}, nil +} + +// builtinWriteFile writes content to a file using os.WriteFile, scoped to project dir. +func (bp *BuiltinProvider) builtinWriteFile(params map[string]string) (*Result, error) { + abs, errResult := bp.scopedPath(params["path"]) + if errResult != nil { + return errResult, nil + } + content := params["content"] + if err := os.MkdirAll(filepath.Dir(abs), 0755); err != nil { + return &Result{Error: err.Error(), ExitCode: 1}, nil + } + if err := os.WriteFile(abs, []byte(content), 0644); err != nil { + return &Result{Error: err.Error(), ExitCode: 1}, nil + } + return &Result{Output: fmt.Sprintf("wrote %d bytes to %s", len(content), abs)}, nil +} + + +// shellCmd returns the platform-appropriate shell and flag for command execution. +func shellCmd() (string, string) { + if runtime.GOOS == "windows" { + return "cmd", "/C" + } + return "sh", "-c" +} + +// builtinShell executes a shell command with a timeout. +func builtinShell(params map[string]string) (*Result, error) { + command := params["command"] + if command == "" { + return &Result{Error: "command is required", ExitCode: 1}, nil + } + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + shell, flag := shellCmd() + cmd := exec.CommandContext(ctx, shell, flag, command) + var stdout, stderr strings.Builder + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + err := cmd.Run() + exitCode := 0 + if err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + exitCode = exitErr.ExitCode() + } else if ctx.Err() == context.DeadlineExceeded { + return &Result{Error: "command timed out after 30s", ExitCode: 124}, nil + } else { + return &Result{Error: err.Error(), ExitCode: 1}, nil + } + } + return &Result{ + Output: stdout.String(), + Error: stderr.String(), + ExitCode: exitCode, + }, nil +} + +// builtinFetch performs an HTTP GET using net/http. +func builtinFetch(params map[string]string) (*Result, error) { + url := params["url"] + if url == "" { + return &Result{Error: "url is required", ExitCode: 1}, nil + } + if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") { + return &Result{Error: "url must start with http:// or https://", ExitCode: 1}, nil + } + + client := &http.Client{Timeout: 30 * time.Second} + resp, err := client.Get(url) + if err != nil { + return &Result{Error: err.Error(), ExitCode: 1}, nil + } + defer resp.Body.Close() + + body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) // 1MB limit + if err != nil { + return &Result{Error: err.Error(), ExitCode: 1}, nil + } + + if resp.StatusCode >= 400 { + return &Result{ + Output: string(body), + Error: fmt.Sprintf("HTTP %d %s", resp.StatusCode, resp.Status), + ExitCode: 1, + }, nil + } + return &Result{Output: string(body)}, nil +} + +// builtinClipboard copies text to the system clipboard. +func builtinClipboard(params map[string]string) (*Result, error) { + text := params["text"] + if text == "" { + return &Result{Error: "text is required", ExitCode: 1}, nil + } + + cmd, args := clipboardCmd() + c := exec.Command(cmd, args...) + c.Stdin = strings.NewReader(text) + + if err := c.Run(); err != nil { + return &Result{Error: err.Error(), ExitCode: 1}, nil + } + return &Result{Output: fmt.Sprintf("copied %d bytes to clipboard", len(text))}, nil +} + +// clipboardCmd returns the platform-specific clipboard command. +func clipboardCmd() (string, []string) { + switch runtime.GOOS { + case "darwin": + return "pbcopy", nil + case "windows": + return "clip", nil + default: + if _, err := exec.LookPath("xsel"); err == nil { + return "xsel", []string{"--clipboard", "--input"} + } + return "xclip", []string{"-selection", "clipboard"} + } +} diff --git a/src/internal/provider/builtin_test.go b/src/internal/provider/builtin_test.go new file mode 100644 index 0000000..c435684 --- /dev/null +++ b/src/internal/provider/builtin_test.go @@ -0,0 +1,210 @@ +// Copyright 2026 Jordan Sherer +// SPDX-License-Identifier: gpl + +package provider + +import ( + "os" + "path/filepath" + "testing" +) + +func TestBuiltinReadFile(t *testing.T) { + tmp := filepath.Join(t.TempDir(), "test.txt") + if err := os.WriteFile(tmp, []byte("hello world"), 0644); err != nil { + t.Fatal(err) + } + + bp := NewBuiltinProvider("stdio", "") + result, err := bp.Execute("factorly.read_file", map[string]string{"path": tmp}) + if err != nil { + t.Fatal(err) + } + if result.Output != "hello world" { + t.Errorf("got %q, want %q", result.Output, "hello world") + } + if result.ExitCode != 0 { + t.Errorf("got exit code %d, want 0", result.ExitCode) + } +} + +func TestBuiltinReadFile_NotFound(t *testing.T) { + bp := NewBuiltinProvider("stdio", "") + result, err := bp.Execute("factorly.read_file", map[string]string{"path": "/nonexistent/file.txt"}) + if err != nil { + t.Fatal(err) + } + if result.ExitCode != 1 { + t.Errorf("got exit code %d, want 1", result.ExitCode) + } + if result.Error == "" { + t.Error("expected error message") + } +} + +func TestBuiltinWriteFile(t *testing.T) { + tmp := filepath.Join(t.TempDir(), "sub", "out.txt") + + bp := NewBuiltinProvider("stdio", "") + result, err := bp.Execute("factorly.write_file", map[string]string{ + "path": tmp, + "content": "test content", + }) + if err != nil { + t.Fatal(err) + } + if result.ExitCode != 0 { + t.Errorf("got exit code %d, want 0", result.ExitCode) + } + + data, err := os.ReadFile(tmp) + if err != nil { + t.Fatal(err) + } + if string(data) != "test content" { + t.Errorf("got %q, want %q", string(data), "test content") + } +} + +func TestBuiltinShell(t *testing.T) { + bp := NewBuiltinProvider("stdio", "") + result, err := bp.Execute("factorly.shell", map[string]string{"command": "echo hello"}) + if err != nil { + t.Fatal(err) + } + if result.ExitCode != 0 { + t.Errorf("got exit code %d, want 0", result.ExitCode) + } + if result.Output != "hello\n" { + t.Errorf("got %q, want %q", result.Output, "hello\n") + } +} + +func TestBuiltinShell_ExitCode(t *testing.T) { + bp := NewBuiltinProvider("stdio", "") + result, err := bp.Execute("factorly.shell", map[string]string{"command": "exit 42"}) + if err != nil { + t.Fatal(err) + } + if result.ExitCode != 42 { + t.Errorf("got exit code %d, want 42", result.ExitCode) + } +} + +func TestBuiltinFetch_BadScheme(t *testing.T) { + bp := NewBuiltinProvider("stdio", "") + result, err := bp.Execute("factorly.fetch", map[string]string{"url": "ftp://evil.com"}) + if err != nil { + t.Fatal(err) + } + if result.ExitCode != 1 { + t.Errorf("got exit code %d, want 1", result.ExitCode) + } + if result.Error == "" { + t.Error("expected error for non-http scheme") + } +} + +func TestBuiltinProvider_HttpMode(t *testing.T) { + bp := NewBuiltinProvider("http", "") + + // fetch should work + _, err := bp.Execute("factorly.fetch", map[string]string{"url": "https://example.com"}) + if err != nil { + t.Errorf("fetch should work in http mode: %v", err) + } + + // local tools should not be available + _, err = bp.Execute("factorly.read_file", map[string]string{"path": "/tmp/x"}) + if err == nil { + t.Error("read_file should not be available in http mode") + } + + _, err = bp.Execute("factorly.shell", map[string]string{"command": "echo x"}) + if err == nil { + t.Error("shell should not be available in http mode") + } +} + +func TestBuiltinProvider_UnknownTool(t *testing.T) { + bp := NewBuiltinProvider("stdio", "") + _, err := bp.Execute("factorly.nonexistent", map[string]string{}) + if err == nil { + t.Error("expected error for unknown tool") + } +} + +func TestBuiltinReadFile_ScopedToProject(t *testing.T) { + root := t.TempDir() + // Create a file inside project + inner := filepath.Join(root, "data.txt") + if err := os.WriteFile(inner, []byte("inside"), 0644); err != nil { + t.Fatal(err) + } + // Create a file outside project + outside := filepath.Join(t.TempDir(), "secret.txt") + if err := os.WriteFile(outside, []byte("secret"), 0644); err != nil { + t.Fatal(err) + } + + bp := NewBuiltinProvider("stdio", root) + + // Should succeed for file inside project + result, err := bp.Execute("factorly.read_file", map[string]string{"path": "data.txt"}) + if err != nil { + t.Fatal(err) + } + if result.ExitCode != 0 { + t.Errorf("expected success, got error: %s", result.Error) + } + if result.Output != "inside" { + t.Errorf("got %q, want %q", result.Output, "inside") + } + + // Should fail for absolute path outside project + result, err = bp.Execute("factorly.read_file", map[string]string{"path": outside}) + if err != nil { + t.Fatal(err) + } + if result.ExitCode != 1 { + t.Error("expected error for path outside project") + } + + // Should fail for traversal attempt + result, err = bp.Execute("factorly.read_file", map[string]string{"path": "../../../etc/passwd"}) + if err != nil { + t.Fatal(err) + } + if result.ExitCode != 1 { + t.Error("expected error for path traversal") + } +} + +func TestBuiltinWriteFile_ScopedToProject(t *testing.T) { + root := t.TempDir() + bp := NewBuiltinProvider("stdio", root) + + // Should succeed within project + result, err := bp.Execute("factorly.write_file", map[string]string{ + "path": "output.txt", + "content": "hello", + }) + if err != nil { + t.Fatal(err) + } + if result.ExitCode != 0 { + t.Errorf("expected success, got error: %s", result.Error) + } + + // Should fail for path outside project + result, err = bp.Execute("factorly.write_file", map[string]string{ + "path": "/tmp/escape.txt", + "content": "bad", + }) + if err != nil { + t.Fatal(err) + } + if result.ExitCode != 1 { + t.Error("expected error for path outside project") + } +} diff --git a/src/internal/provider/cli.go b/src/internal/provider/cli.go index ee4beb9..7177166 100644 --- a/src/internal/provider/cli.go +++ b/src/internal/provider/cli.go @@ -35,6 +35,16 @@ func (p *CLIProvider) Setup() error { return nil } +// AddTool registers a new tool definition at runtime. +func (p *CLIProvider) AddTool(name string, def CLIToolDef) { + p.tools[name] = def +} + +// RemoveTool removes a tool definition at runtime. +func (p *CLIProvider) RemoveTool(name string) { + delete(p.tools, name) +} + func (p *CLIProvider) Teardown() error { return nil } diff --git a/src/internal/provider/expr.go b/src/internal/provider/expr.go index 43e890a..7aedd48 100644 --- a/src/internal/provider/expr.go +++ b/src/internal/provider/expr.go @@ -5,14 +5,35 @@ package provider import ( "encoding/json" + "fmt" "strconv" "strings" + "time" "unicode" "github.com/ohler55/ojg/jp" "github.com/ohler55/ojg/oj" ) +// EvalExpr evaluates an expression and returns its string result. +// Used for {{expr:...}} substitution in workflow step params. +func EvalExpr(expr string, vars map[string]string) string { + expr = strings.TrimSpace(expr) + if expr == "" { + return "" + } + tokens := tokenize(expr) + node := parse(tokens) + if node == nil { + return "" + } + val, err := evaluate(node, vars) + if err != nil { + return "" + } + return toString(val) +} + // EvalCondition evaluates a condition expression against workflow variables. // Returns true if the expression is truthy. Errors are treated as false (fail-closed). func EvalCondition(expr string, vars map[string]string) bool { @@ -410,10 +431,214 @@ func evalCall(name string, args []node, vars map[string]string) any { expr, _ := evaluate(args[1], vars) return evalJSONPath(toString(data), toString(expr)) } + case "now": + return evalNow(args, vars) + case "default": + if len(args) >= 2 { + val, _ := evaluate(args[0], vars) + s := toString(val) + if s != "" { + return s + } + fallback, _ := evaluate(args[1], vars) + return toString(fallback) + } + case "upper": + if len(args) >= 1 { + val, _ := evaluate(args[0], vars) + return strings.ToUpper(toString(val)) + } + case "lower": + if len(args) >= 1 { + val, _ := evaluate(args[0], vars) + return strings.ToLower(toString(val)) + } + case "trim": + if len(args) >= 1 { + val, _ := evaluate(args[0], vars) + return strings.TrimSpace(toString(val)) + } + case "len": + if len(args) >= 1 { + val, _ := evaluate(args[0], vars) + return evalLen(toString(val)) + } + case "substr": + return evalSubstr(args, vars) + case "join": + if len(args) >= 2 { + val, _ := evaluate(args[0], vars) + sep, _ := evaluate(args[1], vars) + return evalJoin(toString(val), toString(sep)) + } + case "replace": + if len(args) >= 3 { + val, _ := evaluate(args[0], vars) + old, _ := evaluate(args[1], vars) + new, _ := evaluate(args[2], vars) + return strings.ReplaceAll(toString(val), toString(old), toString(new)) + } + case "today": + return evalToday(args, vars) + case "left": + if len(args) >= 2 { + val, _ := evaluate(args[0], vars) + n, _ := evaluate(args[1], vars) + s := toString(val) + count, _ := strconv.Atoi(fmt.Sprint(n)) + if count > len(s) { + count = len(s) + } + if count < 0 { + count = 0 + } + return s[:count] + } + case "right": + if len(args) >= 2 { + val, _ := evaluate(args[0], vars) + n, _ := evaluate(args[1], vars) + s := toString(val) + count, _ := strconv.Atoi(fmt.Sprint(n)) + if count > len(s) { + count = len(s) + } + if count < 0 { + count = 0 + } + return s[len(s)-count:] + } + case "find": + if len(args) >= 2 { + val, _ := evaluate(args[0], vars) + needle, _ := evaluate(args[1], vars) + idx := strings.Index(toString(val), toString(needle)) + return idx + } + case "cut": + if len(args) >= 2 { + val, _ := evaluate(args[0], vars) + delim, _ := evaluate(args[1], vars) + return evalCut(toString(val), toString(delim), args, vars) + } + case "concat": + var sb strings.Builder + for _, arg := range args { + val, _ := evaluate(arg, vars) + sb.WriteString(toString(val)) + } + return sb.String() } return false } +func evalToday(args []node, vars map[string]string) string { + t := time.Now().UTC() + if len(args) >= 1 { + offset, _ := evaluate(args[0], vars) + s := toString(offset) + // Try duration first (e.g., '24h', '-48h') + if d, err := time.ParseDuration(s); err == nil { + t = t.Add(d) + } else { + // Try day count (e.g., '1', '-7') + if days, err := strconv.Atoi(s); err == nil { + t = t.AddDate(0, 0, days) + } + } + } + return t.Format("2006-01-02") +} + +func evalCut(s, delim string, args []node, vars map[string]string) string { + parts := strings.Split(s, delim) + if len(args) >= 3 { + // cut(val, delim, index) — return specific field + idxVal, _ := evaluate(args[2], vars) + idx, _ := strconv.Atoi(fmt.Sprint(idxVal)) + if idx < 0 { + idx = len(parts) + idx // negative indexing + } + if idx >= 0 && idx < len(parts) { + return parts[idx] + } + return "" + } + // No index — return as JSON array + b, _ := json.Marshal(parts) + return string(b) +} + +func evalNow(args []node, vars map[string]string) string { + t := time.Now().UTC() + if len(args) >= 1 { + offset, _ := evaluate(args[0], vars) + if d, err := time.ParseDuration(toString(offset)); err == nil { + t = t.Add(d) + } + } + return t.Format(time.RFC3339) +} + +func evalLen(s string) int { + // Try as JSON array + var arr []any + if json.Unmarshal([]byte(s), &arr) == nil { + return len(arr) + } + // Try as JSON object + var obj map[string]any + if json.Unmarshal([]byte(s), &obj) == nil { + return len(obj) + } + return len(s) +} + +func evalSubstr(args []node, vars map[string]string) string { + if len(args) < 2 { + return "" + } + val, _ := evaluate(args[0], vars) + s := toString(val) + startVal, _ := evaluate(args[1], vars) + start, _ := strconv.Atoi(fmt.Sprint(startVal)) + if start < 0 { + start = 0 + } + if start >= len(s) { + return "" + } + end := len(s) + if len(args) >= 3 { + endVal, _ := evaluate(args[2], vars) + if e, err := strconv.Atoi(fmt.Sprint(endVal)); err == nil && e < end { + end = e + } + } + if end > len(s) { + end = len(s) + } + return s[start:end] +} + +func evalJoin(s, sep string) string { + var arr []any + if json.Unmarshal([]byte(s), &arr) != nil { + return s + } + parts := make([]string, len(arr)) + for i, v := range arr { + switch val := v.(type) { + case string: + parts[i] = val + default: + b, _ := json.Marshal(val) + parts[i] = string(b) + } + } + return strings.Join(parts, sep) +} + func evalJSONPath(data, path string) any { parsed, err := jp.ParseString(path) if err != nil { diff --git a/src/internal/provider/expr_test.go b/src/internal/provider/expr_test.go index 95d695c..78c4320 100644 --- a/src/internal/provider/expr_test.go +++ b/src/internal/provider/expr_test.go @@ -5,7 +5,9 @@ package provider import ( "fmt" + "strings" "testing" + "time" ) func TestExprStringEquality(t *testing.T) { @@ -721,3 +723,497 @@ func TestExprRealWorldConditions(t *testing.T) { t.Error("missing approval should block deploy") } } + +// --- EvalExpr tests --- + +func TestEvalExprNow(t *testing.T) { + result := EvalExpr("now()", nil) + if result == "" { + t.Fatal("now() should return a timestamp") + } + if _, err := time.Parse(time.RFC3339, result); err != nil { + t.Errorf("now() should return RFC3339, got %q: %v", result, err) + } +} + +func TestEvalExprNowWithOffset(t *testing.T) { + result := EvalExpr("now('24h')", nil) + if result == "" { + t.Fatal("now('24h') should return a timestamp") + } + ts, err := time.Parse(time.RFC3339, result) + if err != nil { + t.Fatalf("parse error: %v", err) + } + diff := ts.Sub(time.Now().UTC()) + if diff < 23*time.Hour || diff > 25*time.Hour { + t.Errorf("expected ~24h in future, got %v", diff) + } +} + +func TestEvalExprNowNegativeOffset(t *testing.T) { + result := EvalExpr("now('-1h')", nil) + ts, _ := time.Parse(time.RFC3339, result) + diff := time.Now().UTC().Sub(ts) + if diff < 50*time.Minute || diff > 70*time.Minute { + t.Errorf("expected ~1h in past, got %v", diff) + } +} + +func TestEvalExprNowMinutes(t *testing.T) { + result := EvalExpr("now('30m')", nil) + ts, err := time.Parse(time.RFC3339, result) + if err != nil { + t.Fatalf("parse error: %v", err) + } + diff := ts.Sub(time.Now().UTC()) + if diff < 25*time.Minute || diff > 35*time.Minute { + t.Errorf("expected ~30m in future, got %v", diff) + } +} + +func TestEvalExprNowCombined(t *testing.T) { + result := EvalExpr("now('1h30m')", nil) + ts, err := time.Parse(time.RFC3339, result) + if err != nil { + t.Fatalf("parse error: %v", err) + } + diff := ts.Sub(time.Now().UTC()) + if diff < 85*time.Minute || diff > 95*time.Minute { + t.Errorf("expected ~90m in future, got %v", diff) + } +} + +func TestEvalExprNowInvalidOffset(t *testing.T) { + // Invalid offset should fall back to current time (no offset applied) + result := EvalExpr("now('garbage')", nil) + ts, err := time.Parse(time.RFC3339, result) + if err != nil { + t.Fatalf("parse error: %v", err) + } + diff := time.Since(ts) + if diff < -5*time.Second || diff > 5*time.Second { + t.Errorf("invalid offset should return ~now, got %v diff", diff) + } +} + +func TestEvalExprNowZeroOffset(t *testing.T) { + result := EvalExpr("now('0s')", nil) + ts, err := time.Parse(time.RFC3339, result) + if err != nil { + t.Fatalf("parse error: %v", err) + } + diff := time.Since(ts) + if diff < -5*time.Second || diff > 5*time.Second { + t.Errorf("zero offset should return ~now, got %v diff", diff) + } +} + +func TestEvalExprDefault(t *testing.T) { + vars := map[string]string{"title": "Hello"} + if r := EvalExpr("default(title, 'Untitled')", vars); r != "Hello" { + t.Errorf("expected 'Hello', got %q", r) + } + vars["title"] = "" + if r := EvalExpr("default(title, 'Untitled')", vars); r != "Untitled" { + t.Errorf("expected 'Untitled', got %q", r) + } +} + +func TestEvalExprDefaultMissing(t *testing.T) { + if r := EvalExpr("default(missing, 'fallback')", map[string]string{}); r != "fallback" { + t.Errorf("expected 'fallback', got %q", r) + } +} + +func TestEvalExprUpper(t *testing.T) { + vars := map[string]string{"name": "jordan"} + if r := EvalExpr("upper(name)", vars); r != "JORDAN" { + t.Errorf("expected 'JORDAN', got %q", r) + } +} + +func TestEvalExprLower(t *testing.T) { + vars := map[string]string{"name": "JORDAN"} + if r := EvalExpr("lower(name)", vars); r != "jordan" { + t.Errorf("expected 'jordan', got %q", r) + } +} + +func TestEvalExprTrim(t *testing.T) { + vars := map[string]string{"output": " hello world \n"} + if r := EvalExpr("trim(output)", vars); r != "hello world" { + t.Errorf("expected 'hello world', got %q", r) + } +} + +func TestEvalExprLen(t *testing.T) { + vars := map[string]string{"items": `["a","b","c"]`} + if r := EvalExpr("len(items)", vars); r != "3" { + t.Errorf("expected '3', got %q", r) + } +} + +func TestEvalExprLenString(t *testing.T) { + vars := map[string]string{"name": "hello"} + if r := EvalExpr("len(name)", vars); r != "5" { + t.Errorf("expected '5', got %q", r) + } +} + +func TestEvalExprLenObject(t *testing.T) { + vars := map[string]string{"data": `{"a":1,"b":2}`} + if r := EvalExpr("len(data)", vars); r != "2" { + t.Errorf("expected '2', got %q", r) + } +} + +func TestEvalExprSubstr(t *testing.T) { + vars := map[string]string{"id": "abcdefghij"} + if r := EvalExpr("substr(id, 0, 4)", vars); r != "abcd" { + t.Errorf("expected 'abcd', got %q", r) + } +} + +func TestEvalExprSubstrNoEnd(t *testing.T) { + vars := map[string]string{"id": "abcdefghij"} + if r := EvalExpr("substr(id, 5)", vars); r != "fghij" { + t.Errorf("expected 'fghij', got %q", r) + } +} + +func TestEvalExprJoin(t *testing.T) { + vars := map[string]string{"tags": `["go","rust","python"]`} + if r := EvalExpr("join(tags, ', ')", vars); r != "go, rust, python" { + t.Errorf("expected 'go, rust, python', got %q", r) + } +} + +func TestEvalExprReplace(t *testing.T) { + vars := map[string]string{"output": "hello\nworld\n"} + if r := EvalExpr("replace(output, '\n', ' ')", vars); !strings.Contains(r, "hello") { + t.Errorf("expected replaced output, got %q", r) + } +} + +func TestEvalExprJSONPath(t *testing.T) { + vars := map[string]string{"data": `{"items":[{"name":"first"},{"name":"second"}]}`} + if r := EvalExpr("jsonpath(data, '$.items[0].name')", vars); r != "first" { + t.Errorf("expected 'first', got %q", r) + } +} + +func TestEvalExprEmpty(t *testing.T) { + if r := EvalExpr("", nil); r != "" { + t.Errorf("expected empty, got %q", r) + } +} + +func TestEvalExprLiteral(t *testing.T) { + if r := EvalExpr("'hello'", nil); r != "hello" { + t.Errorf("expected 'hello', got %q", r) + } +} + +// --- Edge case tests for expression functions --- + +func TestEvalExprUpperEmpty(t *testing.T) { + vars := map[string]string{"x": ""} + if r := EvalExpr("upper(x)", vars); r != "" { + t.Errorf("expected empty, got %q", r) + } +} + +func TestEvalExprUpperAlready(t *testing.T) { + vars := map[string]string{"x": "ABC"} + if r := EvalExpr("upper(x)", vars); r != "ABC" { + t.Errorf("expected 'ABC', got %q", r) + } +} + +func TestEvalExprLowerEmpty(t *testing.T) { + vars := map[string]string{"x": ""} + if r := EvalExpr("lower(x)", vars); r != "" { + t.Errorf("expected empty, got %q", r) + } +} + +func TestEvalExprTrimEmpty(t *testing.T) { + vars := map[string]string{"x": ""} + if r := EvalExpr("trim(x)", vars); r != "" { + t.Errorf("expected empty, got %q", r) + } +} + +func TestEvalExprTrimNoWhitespace(t *testing.T) { + vars := map[string]string{"x": "clean"} + if r := EvalExpr("trim(x)", vars); r != "clean" { + t.Errorf("expected 'clean', got %q", r) + } +} + +func TestEvalExprLenEmptyArray(t *testing.T) { + vars := map[string]string{"items": `[]`} + if r := EvalExpr("len(items)", vars); r != "0" { + t.Errorf("expected '0', got %q", r) + } +} + +func TestEvalExprLenEmptyString(t *testing.T) { + vars := map[string]string{"x": ""} + if r := EvalExpr("len(x)", vars); r != "0" { + t.Errorf("expected '0', got %q", r) + } +} + +func TestEvalExprSubstrOutOfBounds(t *testing.T) { + vars := map[string]string{"x": "abc"} + if r := EvalExpr("substr(x, 10)", vars); r != "" { + t.Errorf("expected empty for out of bounds start, got %q", r) + } +} + +func TestEvalExprSubstrEndBeyondLength(t *testing.T) { + vars := map[string]string{"x": "abc"} + if r := EvalExpr("substr(x, 0, 100)", vars); r != "abc" { + t.Errorf("expected 'abc' when end > len, got %q", r) + } +} + +func TestEvalExprSubstrEmpty(t *testing.T) { + vars := map[string]string{"x": ""} + if r := EvalExpr("substr(x, 0, 5)", vars); r != "" { + t.Errorf("expected empty, got %q", r) + } +} + +func TestEvalExprJoinEmptyArray(t *testing.T) { + vars := map[string]string{"items": `[]`} + if r := EvalExpr("join(items, ', ')", vars); r != "" { + t.Errorf("expected empty, got %q", r) + } +} + +func TestEvalExprJoinSingleItem(t *testing.T) { + vars := map[string]string{"items": `["only"]`} + if r := EvalExpr("join(items, ', ')", vars); r != "only" { + t.Errorf("expected 'only', got %q", r) + } +} + +func TestEvalExprJoinNonArray(t *testing.T) { + vars := map[string]string{"x": "not json"} + if r := EvalExpr("join(x, ', ')", vars); r != "not json" { + t.Errorf("expected passthrough for non-array, got %q", r) + } +} + +func TestEvalExprReplaceNoMatch(t *testing.T) { + vars := map[string]string{"x": "hello"} + if r := EvalExpr("replace(x, 'z', 'a')", vars); r != "hello" { + t.Errorf("expected 'hello' unchanged, got %q", r) + } +} + +func TestEvalExprReplaceMultiple(t *testing.T) { + vars := map[string]string{"x": "aaa"} + if r := EvalExpr("replace(x, 'a', 'b')", vars); r != "bbb" { + t.Errorf("expected 'bbb', got %q", r) + } +} + +func TestEvalExprReplaceEmpty(t *testing.T) { + vars := map[string]string{"x": ""} + if r := EvalExpr("replace(x, 'a', 'b')", vars); r != "" { + t.Errorf("expected empty, got %q", r) + } +} + +func TestEvalExprJSONPathNoMatch(t *testing.T) { + vars := map[string]string{"data": `{"a":1}`} + if r := EvalExpr("jsonpath(data, '$.nonexistent')", vars); r != "" { + t.Errorf("expected empty for no match, got %q", r) + } +} + +func TestEvalExprJSONPathNested(t *testing.T) { + vars := map[string]string{"data": `{"a":{"b":{"c":"deep"}}}`} + if r := EvalExpr("jsonpath(data, '$.a.b.c')", vars); r != "deep" { + t.Errorf("expected 'deep', got %q", r) + } +} + +func TestEvalExprJSONPathArrayResult(t *testing.T) { + vars := map[string]string{"data": `{"items":[{"id":1},{"id":2}]}`} + r := EvalExpr("jsonpath(data, '$.items[*].id')", vars) + if !strings.Contains(r, "1") || !strings.Contains(r, "2") { + t.Errorf("expected array with 1 and 2, got %q", r) + } +} + +func TestEvalExprDefaultBothEmpty(t *testing.T) { + vars := map[string]string{"x": ""} + if r := EvalExpr("default(x, '')", vars); r != "" { + t.Errorf("expected empty, got %q", r) + } +} + +// --- today(), left(), right(), find(), cut() tests --- + +func TestEvalExprToday(t *testing.T) { + result := EvalExpr("today()", nil) + expected := time.Now().UTC().Format("2006-01-02") + if result != expected { + t.Errorf("expected %q, got %q", expected, result) + } +} + +func TestEvalExprTodayOffset(t *testing.T) { + result := EvalExpr("today('24h')", nil) + expected := time.Now().UTC().Add(24 * time.Hour).Format("2006-01-02") + if result != expected { + t.Errorf("expected %q, got %q", expected, result) + } +} + +func TestEvalExprTodayDayCount(t *testing.T) { + result := EvalExpr("today('7')", nil) + expected := time.Now().UTC().AddDate(0, 0, 7).Format("2006-01-02") + if result != expected { + t.Errorf("expected %q, got %q", expected, result) + } +} + +func TestEvalExprTodayNegative(t *testing.T) { + result := EvalExpr("today('-1')", nil) + expected := time.Now().UTC().AddDate(0, 0, -1).Format("2006-01-02") + if result != expected { + t.Errorf("expected yesterday %q, got %q", expected, result) + } +} + +func TestEvalExprLeft(t *testing.T) { + vars := map[string]string{"s": "abcdefgh"} + if r := EvalExpr("left(s, 3)", vars); r != "abc" { + t.Errorf("expected 'abc', got %q", r) + } +} + +func TestEvalExprLeftBeyondLength(t *testing.T) { + vars := map[string]string{"s": "ab"} + if r := EvalExpr("left(s, 10)", vars); r != "ab" { + t.Errorf("expected 'ab', got %q", r) + } +} + +func TestEvalExprLeftEmpty(t *testing.T) { + vars := map[string]string{"s": ""} + if r := EvalExpr("left(s, 3)", vars); r != "" { + t.Errorf("expected empty, got %q", r) + } +} + +func TestEvalExprRight(t *testing.T) { + vars := map[string]string{"s": "abcdefgh"} + if r := EvalExpr("right(s, 3)", vars); r != "fgh" { + t.Errorf("expected 'fgh', got %q", r) + } +} + +func TestEvalExprRightBeyondLength(t *testing.T) { + vars := map[string]string{"s": "ab"} + if r := EvalExpr("right(s, 10)", vars); r != "ab" { + t.Errorf("expected 'ab', got %q", r) + } +} + +func TestEvalExprRightEmpty(t *testing.T) { + vars := map[string]string{"s": ""} + if r := EvalExpr("right(s, 3)", vars); r != "" { + t.Errorf("expected empty, got %q", r) + } +} + +func TestEvalExprFind(t *testing.T) { + vars := map[string]string{"s": "hello world"} + if r := EvalExpr("find(s, 'world')", vars); r != "6" { + t.Errorf("expected '6', got %q", r) + } +} + +func TestEvalExprFindNotFound(t *testing.T) { + vars := map[string]string{"s": "hello"} + if r := EvalExpr("find(s, 'xyz')", vars); r != "-1" { + t.Errorf("expected '-1', got %q", r) + } +} + +func TestEvalExprFindEmpty(t *testing.T) { + vars := map[string]string{"s": "hello"} + if r := EvalExpr("find(s, '')", vars); r != "0" { + t.Errorf("expected '0', got %q", r) + } +} + +func TestEvalExprCutWithIndex(t *testing.T) { + vars := map[string]string{"s": "one:two:three"} + if r := EvalExpr("cut(s, ':', 0)", vars); r != "one" { + t.Errorf("expected 'one', got %q", r) + } + if r := EvalExpr("cut(s, ':', 1)", vars); r != "two" { + t.Errorf("expected 'two', got %q", r) + } + if r := EvalExpr("cut(s, ':', 2)", vars); r != "three" { + t.Errorf("expected 'three', got %q", r) + } +} + +func TestEvalExprCutNegativeIndex(t *testing.T) { + vars := map[string]string{"s": "a/b/c/d"} + // Use string '-1' since the parser treats bare -1 as binary minus + if r := EvalExpr("cut(s, '/', '-1')", vars); r != "d" { + t.Errorf("expected 'd', got %q", r) + } +} + +func TestEvalExprCutNoIndex(t *testing.T) { + vars := map[string]string{"s": "a,b,c"} + r := EvalExpr("cut(s, ',')", vars) + if !strings.Contains(r, "a") || !strings.Contains(r, "b") || !strings.Contains(r, "c") { + t.Errorf("expected JSON array, got %q", r) + } +} + +func TestEvalExprCutOutOfBounds(t *testing.T) { + vars := map[string]string{"s": "a:b"} + if r := EvalExpr("cut(s, ':', 5)", vars); r != "" { + t.Errorf("expected empty for out of bounds, got %q", r) + } +} + +func TestEvalExprConcat(t *testing.T) { + vars := map[string]string{"first": "hello", "second": "world"} + if r := EvalExpr("concat(first, ' ', second)", vars); r != "hello world" { + t.Errorf("expected 'hello world', got %q", r) + } +} + +func TestEvalExprConcatSingle(t *testing.T) { + vars := map[string]string{"x": "solo"} + if r := EvalExpr("concat(x)", vars); r != "solo" { + t.Errorf("expected 'solo', got %q", r) + } +} + +func TestEvalExprConcatEmpty(t *testing.T) { + if r := EvalExpr("concat()", nil); r != "" { + t.Errorf("expected empty, got %q", r) + } +} + +func TestEvalExprConcatNested(t *testing.T) { + vars := map[string]string{"name": "jordan"} + if r := EvalExpr("concat(upper(name), '@example.com')", vars); r != "JORDAN@example.com" { + t.Errorf("expected 'JORDAN@example.com', got %q", r) + } +} diff --git a/src/internal/provider/rest.go b/src/internal/provider/rest.go index de6c021..8266449 100644 --- a/src/internal/provider/rest.go +++ b/src/internal/provider/rest.go @@ -12,6 +12,8 @@ import ( "net/http" "net/url" "os" + "path/filepath" + "regexp" "strings" "sync" "time" @@ -44,14 +46,16 @@ type AuthDef struct { } type RESTToolDef struct { - Method string - BaseURL string - Path string - Body string // JSON body template with {{param}} placeholders - Headers map[string]string - Auth *AuthDef - Params []RESTParamDef - Timeout time.Duration + Method string + BaseURL string + Path string + Body string // JSON body template with {{param}} placeholders + BodyType string // "json" (default), "form", "raw" + Headers map[string]string + Auth *AuthDef + Params []RESTParamDef + Timeout time.Duration + RedactSecrets func(s string) string // redacts resolved vault values from a string for verbose logging } type RESTProvider struct { @@ -71,6 +75,16 @@ func (p *RESTProvider) Setup() error { return nil } +// AddTool registers a new tool definition at runtime. +func (p *RESTProvider) AddTool(name string, def RESTToolDef) { + p.tools[name] = def +} + +// RemoveTool removes a tool definition at runtime. +func (p *RESTProvider) RemoveTool(name string) { + delete(p.tools, name) +} + func (p *RESTProvider) Teardown() error { if p.client != nil { p.client.CloseIdleConnections() @@ -108,10 +122,23 @@ func (p *RESTProvider) Execute(toolName string, params map[string]string) (*Resu bodyParams := make(map[string]string) var fileParam string // path to file for in:file + // Pre-scan path for {{param}} placeholders so we can auto-route + // params that match, even if they lack an explicit "in: path". + pathPlaceholders := make(map[string]bool) + if strings.Contains(def.Path, "{{") { + for _, m := range regexp.MustCompile(`\{\{(\w+)\}\}`).FindAllStringSubmatch(def.Path, -1) { + pathPlaceholders[m[1]] = true + } + } + for name, value := range params { in := paramIndex[name] if in == "" { - in = defaultParamLocation(def.Method) + if pathPlaceholders[name] { + in = "path" + } else { + in = defaultParamLocation(def.Method) + } } switch in { case "path": @@ -167,40 +194,53 @@ func (p *RESTProvider) Execute(toolName string, params map[string]string) (*Resu // Apply remaining defaults for unmatched {{param|default}} placeholders bodyContent = applyBodyDefaults(bodyContent) - } else if len(bodyParams) == 1 { - for _, v := range bodyParams { - bodyContent = v + } else if len(bodyParams) > 0 { + bodyType := def.BodyType + if bodyType == "" { + bodyType = "json" // default } - } else if len(bodyParams) > 1 { - obj := make(map[string]json.RawMessage, len(bodyParams)) - for k, v := range bodyParams { - switch paramType[k] { - case "integer", "number": - // Pass numeric values unquoted - obj[k] = json.RawMessage(v) - case "boolean": - // Pass boolean values unquoted - obj[k] = json.RawMessage(v) - case "json": - // Pass raw JSON through (arrays, objects) - obj[k] = json.RawMessage(v) - default: - // "string" or unspecified — auto-detect: - // if valid JSON (array, object, number, bool), pass through; - // otherwise quote as string - if json.Valid([]byte(v)) && (v[0] == '[' || v[0] == '{') { + + switch bodyType { + case "raw": + // Single raw body — use the first (or only) param value as-is + for _, v := range bodyParams { + bodyContent = v + break + } + + case "form": + // URL-encoded form body + form := url.Values{} + for k, v := range bodyParams { + form.Set(k, v) + } + bodyContent = form.Encode() + + default: // "json" + obj := make(map[string]json.RawMessage, len(bodyParams)) + for k, v := range bodyParams { + switch paramType[k] { + case "integer", "number": obj[k] = json.RawMessage(v) - } else { - quoted, _ := json.Marshal(v) - obj[k] = json.RawMessage(quoted) + case "boolean": + obj[k] = json.RawMessage(v) + case "json": + obj[k] = json.RawMessage(v) + default: + if json.Valid([]byte(v)) && len(v) > 0 && (v[0] == '[' || v[0] == '{') { + obj[k] = json.RawMessage(v) + } else { + quoted, _ := json.Marshal(v) + obj[k] = json.RawMessage(quoted) + } } } + data, err := json.Marshal(obj) + if err != nil { + return nil, fmt.Errorf("rest provider: building request body for tool %q: %w", toolName, err) + } + bodyContent = string(data) } - data, err := json.Marshal(obj) - if err != nil { - return nil, fmt.Errorf("rest provider: building request body for tool %q: %w", toolName, err) - } - bodyContent = string(data) } // Substitute path parameters @@ -231,6 +271,9 @@ func (p *RESTProvider) Execute(toolName string, params map[string]string) (*Resu q := parsedURL.Query() for name, value := range queryParams { + if value == "" { + continue // skip empty optional query params + } q.Set(name, value) } parsedURL.RawQuery = q.Encode() @@ -239,7 +282,12 @@ func (p *RESTProvider) Execute(toolName string, params map[string]string) (*Resu var reqBody io.Reader if fileParam != "" { // in:file — stream file as raw binary body - f, err := os.Open(fileParam) + // Resolve to absolute path to prevent ambiguity + absFile, err := filepath.Abs(fileParam) + if err != nil { + return nil, fmt.Errorf("rest provider: invalid file path %q for tool %q: %w", fileParam, toolName, err) + } + f, err := os.Open(absFile) // #nosec G304 -- file path is an explicit tool parameter if err != nil { return nil, fmt.Errorf("rest provider: opening file %q for tool %q: %w", fileParam, toolName, err) } @@ -281,16 +329,54 @@ func (p *RESTProvider) Execute(toolName string, params map[string]string) (*Resu req.Header.Set(name, value) } if bodyContent != "" && req.Header.Get("Content-Type") == "" { - req.Header.Set("Content-Type", "application/json") + if def.BodyType == "form" { + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + } else { + req.Header.Set("Content-Type", "application/json") + } } - // Verbose logging + // Verbose logging (redact secrets) if p.Verbose { - fmt.Fprintf(os.Stderr, "[rest] %s %s\n", req.Method, req.URL) + redact := func(s string) string { + // Redact vault-resolved secrets + if def.RedactSecrets != nil { + s = def.RedactSecrets(s) + } + // Always redact auth credentials (hardcoded or vault-resolved) + if def.Auth != nil { + if def.Auth.Token != "" { + s = strings.ReplaceAll(s, def.Auth.Token, "****") + } + if def.Auth.Value != "" { + s = strings.ReplaceAll(s, def.Auth.Value, "****") + } + } + return s + } + + fmt.Fprintf(os.Stderr, "[rest] %s %s\n", req.Method, redact(req.URL.String())) + for k, vals := range req.Header { + for _, v := range vals { + if isSensitiveHeader(k) { + fmt.Fprintf(os.Stderr, "[rest] header: %s: ****\n", k) + } else { + fmt.Fprintf(os.Stderr, "[rest] header: %s: %s\n", k, redact(v)) + } + } + } + if bodyContent != "" { + bt := def.BodyType + if bt == "" { + bt = "json" + } + fmt.Fprintf(os.Stderr, "[rest] body (%s, %d bytes): %s\n", bt, len(bodyContent), redact(bodyContent)) + } if fileParam != "" { - fi, _ := os.Stat(fileParam) + absFile, _ := filepath.Abs(fileParam) + fi, _ := os.Stat(absFile) // #nosec G304 -- verbose logging for user-provided file param if fi != nil { - fmt.Fprintf(os.Stderr, "[rest] file: %s (%d bytes)\n", fileParam, fi.Size()) + fmt.Fprintf(os.Stderr, "[rest] file: %s (%d bytes)\n", absFile, fi.Size()) } } fmt.Fprintf(os.Stderr, "[rest] timeout: %s\n", timeout) @@ -420,6 +506,15 @@ func applyBodyDefaults(s string) string { return s } +func isSensitiveHeader(name string) bool { + lower := strings.ToLower(name) + return strings.Contains(lower, "auth") || + strings.Contains(lower, "key") || + strings.Contains(lower, "secret") || + strings.Contains(lower, "token") || + strings.Contains(lower, "cookie") +} + func defaultParamLocation(method string) string { switch method { case "POST", "PUT", "PATCH": diff --git a/src/internal/provider/rest_test.go b/src/internal/provider/rest_test.go index 27ead6d..81524c9 100644 --- a/src/internal/provider/rest_test.go +++ b/src/internal/provider/rest_test.go @@ -6,6 +6,7 @@ package provider import ( "bytes" "encoding/base64" + "encoding/json" "fmt" "io" "net/http" @@ -219,10 +220,11 @@ func TestRESTBodyParam(t *testing.T) { p := NewREST(map[string]RESTToolDef{ "test": { - Method: "POST", - BaseURL: srv.URL, - Path: "/items", - Params: []RESTParamDef{{Name: "body", In: "body"}}, + Method: "POST", + BaseURL: srv.URL, + Path: "/items", + BodyType: "raw", + Params: []RESTParamDef{{Name: "body", In: "body"}}, }, }, nil) _ = p.Setup() @@ -281,9 +283,10 @@ func TestRESTDefaultParamRoutingPOST(t *testing.T) { p := NewREST(map[string]RESTToolDef{ "test": { - Method: "POST", - BaseURL: srv.URL, - Path: "/items", + Method: "POST", + BaseURL: srv.URL, + Path: "/items", + BodyType: "raw", }, }, nil) _ = p.Setup() @@ -291,7 +294,7 @@ func TestRESTDefaultParamRoutingPOST(t *testing.T) { _, _ = p.Execute("test", map[string]string{"data": `{"x":1}`}) if capturedBody != `{"x":1}` { - t.Errorf("expected POST param as body, got %q", capturedBody) + t.Errorf("expected POST param as raw body, got %q", capturedBody) } } @@ -316,9 +319,10 @@ func TestRESTMixedParams(t *testing.T) { p := NewREST(map[string]RESTToolDef{ "test": { - Method: "POST", - BaseURL: srv.URL, - Path: "/orgs/{{orgId}}/items", + Method: "POST", + BaseURL: srv.URL, + Path: "/orgs/{{orgId}}/items", + BodyType: "raw", Params: []RESTParamDef{ {Name: "orgId", In: "path", Required: true}, {Name: "limit", In: "query"}, @@ -540,11 +544,12 @@ func TestRESTContentTypeOverride(t *testing.T) { p := NewREST(map[string]RESTToolDef{ "test": { - Method: "POST", - BaseURL: srv.URL, - Path: "/", - Headers: map[string]string{"Content-Type": "text/plain"}, - Params: []RESTParamDef{{Name: "body", In: "body"}}, + Method: "POST", + BaseURL: srv.URL, + Path: "/", + BodyType: "raw", + Headers: map[string]string{"Content-Type": "text/plain"}, + Params: []RESTParamDef{{Name: "body", In: "body"}}, }, }, nil) _ = p.Setup() @@ -556,6 +561,32 @@ func TestRESTContentTypeOverride(t *testing.T) { } } +func TestRESTParamHeaderOverridesStatic(t *testing.T) { + var capturedTenant string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + capturedTenant = r.Header.Get("X-Tenant") + w.WriteHeader(200) + })) + defer srv.Close() + + p := NewREST(map[string]RESTToolDef{ + "test": { + Method: "GET", + BaseURL: srv.URL, + Path: "/", + Headers: map[string]string{"X-Tenant": "default-tenant"}, + Params: []RESTParamDef{{Name: "X-Tenant", In: "header"}}, + }, + }, nil) + _ = p.Setup() + + // Param header should override the static header + _, _ = p.Execute("test", map[string]string{"X-Tenant": "custom-tenant"}) + if capturedTenant != "custom-tenant" { + t.Errorf("expected param header to override static, got %q", capturedTenant) + } +} + // --- Response Handling --- func TestRESTSuccess200(t *testing.T) { @@ -1020,6 +1051,512 @@ func TestRESTFileParamMissing(t *testing.T) { t.Fatal("expected error for missing file") } if !strings.Contains(err.Error(), "opening file") { - t.Errorf("expected 'reading file' in error, got: %s", err.Error()) + t.Errorf("expected 'opening file' in error, got: %s", err.Error()) + } +} + +func TestRESTFileParamRelativePath(t *testing.T) { + // Relative paths should be resolved to absolute via filepath.Abs + // This test ensures relative file paths work (no panic/weird behavior) + tmpFile := filepath.Join(t.TempDir(), "upload.bin") + if err := os.WriteFile(tmpFile, []byte("test"), 0o644); err != nil { + t.Fatal(err) + } + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, _ := io.ReadAll(r.Body) + _, _ = w.Write(body) + })) + defer srv.Close() + + p := NewREST(map[string]RESTToolDef{ + "upload": { + Method: "POST", + BaseURL: srv.URL, + Path: "/upload", + Params: []RESTParamDef{{Name: "file", In: "file", Required: true}}, + }, + }, nil) + _ = p.Setup() + + // Use absolute path (relative would depend on CWD) + result, err := p.Execute("upload", map[string]string{ + "file": tmpFile, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if result.Output != "test" { + t.Errorf("expected 'test', got %q", result.Output) + } +} + +func TestRESTPathParamsAutoDetect(t *testing.T) { + // Params without "in: path" should auto-route to path if they match + // a {{param}} placeholder in the path template. + var receivedPath string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + receivedPath = r.URL.Path + _, _ = w.Write([]byte("ok")) + })) + defer srv.Close() + + p := NewREST(map[string]RESTToolDef{ + "repos.get": { + Method: "GET", + BaseURL: srv.URL, + Path: "/repos/{{owner}}/{{repo}}", + Params: []RESTParamDef{ + {Name: "owner", Required: true}, // no In field + {Name: "repo", Required: true}, // no In field + }, + }, + }, nil) + _ = p.Setup() + + result, err := p.Execute("repos.get", map[string]string{ + "owner": "factorly-dev", + "repo": "factorly", + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if result.Output != "ok" { + t.Errorf("expected 'ok', got %q", result.Output) + } + if receivedPath != "/repos/factorly-dev/factorly" { + t.Errorf("expected /repos/factorly-dev/factorly, got %s", receivedPath) + } +} + +// --- Body Type tests --- + +func TestRESTBodyType_JSONSingleParam(t *testing.T) { + var capturedBody string + var capturedCT string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + capturedCT = r.Header.Get("Content-Type") + body, _ := io.ReadAll(r.Body) + capturedBody = string(body) + _, _ = w.Write([]byte("ok")) + })) + defer srv.Close() + + p := NewREST(map[string]RESTToolDef{ + "test": { + Method: "POST", + BaseURL: srv.URL, + Path: "/getChat", + Params: []RESTParamDef{{Name: "chat_id", In: "body", Required: true}}, + // BodyType defaults to "json" + }, + }, nil) + _ = p.Setup() + + _, err := p.Execute("test", map[string]string{"chat_id": "123456"}) + if err != nil { + t.Fatal(err) + } + if capturedBody != `{"chat_id":"123456"}` { + t.Errorf("expected JSON object, got %q", capturedBody) + } + if capturedCT != "application/json" { + t.Errorf("expected application/json, got %q", capturedCT) + } +} + +func TestRESTBodyType_JSONMultipleParams(t *testing.T) { + var capturedBody string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, _ := io.ReadAll(r.Body) + capturedBody = string(body) + _, _ = w.Write([]byte("ok")) + })) + defer srv.Close() + + p := NewREST(map[string]RESTToolDef{ + "test": { + Method: "POST", + BaseURL: srv.URL, + Path: "/sendMessage", + BodyType: "json", + Params: []RESTParamDef{ + {Name: "chat_id", In: "body", Required: true}, + {Name: "text", In: "body", Required: true}, + }, + }, + }, nil) + _ = p.Setup() + + _, err := p.Execute("test", map[string]string{"chat_id": "123", "text": "hello"}) + if err != nil { + t.Fatal(err) + } + + // Parse as JSON to check keys (order may vary) + var parsed map[string]any + if err := json.Unmarshal([]byte(capturedBody), &parsed); err != nil { + t.Fatalf("body not valid JSON: %s", capturedBody) + } + if parsed["chat_id"] != "123" { + t.Errorf("chat_id: expected '123', got %v", parsed["chat_id"]) + } + if parsed["text"] != "hello" { + t.Errorf("text: expected 'hello', got %v", parsed["text"]) + } +} + +func TestRESTBodyType_JSONWithTypes(t *testing.T) { + var capturedBody string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, _ := io.ReadAll(r.Body) + capturedBody = string(body) + _, _ = w.Write([]byte("ok")) + })) + defer srv.Close() + + p := NewREST(map[string]RESTToolDef{ + "test": { + Method: "POST", + BaseURL: srv.URL, + Path: "/", + Params: []RESTParamDef{ + {Name: "count", In: "body", Type: "integer"}, + {Name: "active", In: "body", Type: "boolean"}, + {Name: "tags", In: "body", Type: "json"}, + {Name: "name", In: "body"}, + }, + }, + }, nil) + _ = p.Setup() + + _, err := p.Execute("test", map[string]string{ + "count": "42", + "active": "true", + "tags": `["a","b"]`, + "name": "test", + }) + if err != nil { + t.Fatal(err) + } + + var parsed map[string]json.RawMessage + if err := json.Unmarshal([]byte(capturedBody), &parsed); err != nil { + t.Fatalf("body not valid JSON: %s", capturedBody) + } + // integer: unquoted + if string(parsed["count"]) != "42" { + t.Errorf("count: expected 42, got %s", parsed["count"]) + } + // boolean: unquoted + if string(parsed["active"]) != "true" { + t.Errorf("active: expected true, got %s", parsed["active"]) + } + // json: raw array + if string(parsed["tags"]) != `["a","b"]` { + t.Errorf("tags: expected [\"a\",\"b\"], got %s", parsed["tags"]) + } + // string: quoted + if string(parsed["name"]) != `"test"` { + t.Errorf("name: expected \"test\", got %s", parsed["name"]) + } +} + +func TestRESTBodyType_Form(t *testing.T) { + var capturedBody string + var capturedCT string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + capturedCT = r.Header.Get("Content-Type") + body, _ := io.ReadAll(r.Body) + capturedBody = string(body) + _, _ = w.Write([]byte("ok")) + })) + defer srv.Close() + + p := NewREST(map[string]RESTToolDef{ + "test": { + Method: "POST", + BaseURL: srv.URL, + Path: "/submit", + BodyType: "form", + Params: []RESTParamDef{ + {Name: "username", In: "body", Required: true}, + {Name: "password", In: "body", Required: true}, + }, + }, + }, nil) + _ = p.Setup() + + _, err := p.Execute("test", map[string]string{"username": "admin", "password": "secret"}) + if err != nil { + t.Fatal(err) + } + + if capturedCT != "application/x-www-form-urlencoded" { + t.Errorf("expected form content-type, got %q", capturedCT) + } + if !strings.Contains(capturedBody, "username=admin") { + t.Errorf("expected username=admin in body, got %q", capturedBody) + } + if !strings.Contains(capturedBody, "password=secret") { + t.Errorf("expected password=secret in body, got %q", capturedBody) + } +} + +func TestRESTBodyType_Raw(t *testing.T) { + var capturedBody string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, _ := io.ReadAll(r.Body) + capturedBody = string(body) + _, _ = w.Write([]byte("ok")) + })) + defer srv.Close() + + p := NewREST(map[string]RESTToolDef{ + "test": { + Method: "POST", + BaseURL: srv.URL, + Path: "/", + BodyType: "raw", + Params: []RESTParamDef{{Name: "payload", In: "body"}}, + }, + }, nil) + _ = p.Setup() + + _, err := p.Execute("test", map[string]string{"payload": `hello`}) + if err != nil { + t.Fatal(err) + } + if capturedBody != `hello` { + t.Errorf("expected raw XML, got %q", capturedBody) + } +} + +func TestRESTBodyType_DefaultIsJSON(t *testing.T) { + // No explicit BodyType — should default to json + var capturedBody string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, _ := io.ReadAll(r.Body) + capturedBody = string(body) + _, _ = w.Write([]byte("ok")) + })) + defer srv.Close() + + p := NewREST(map[string]RESTToolDef{ + "test": { + Method: "POST", + BaseURL: srv.URL, + Path: "/", + Params: []RESTParamDef{{Name: "key", In: "body"}}, + }, + }, nil) + _ = p.Setup() + + _, err := p.Execute("test", map[string]string{"key": "value"}) + if err != nil { + t.Fatal(err) + } + if capturedBody != `{"key":"value"}` { + t.Errorf("expected JSON object, got %q", capturedBody) + } +} + +func TestRESTVerboseRedactsSecrets(t *testing.T) { + secret := "sk_live_test_secret_12345" + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte("ok")) + })) + defer srv.Close() + + p := NewREST(map[string]RESTToolDef{ + "test": { + Method: "GET", + BaseURL: srv.URL, + Path: "/data", + Auth: &AuthDef{Type: "bearer", Token: secret}, + RedactSecrets: func(s string) string { + return strings.ReplaceAll(s, secret, "****") + }, + }, + }, nil) + p.Verbose = true + _ = p.Setup() + + // Capture stderr + oldStderr := os.Stderr + r, w, _ := os.Pipe() + os.Stderr = w + + _, _ = p.Execute("test", map[string]string{}) + + _ = w.Close() + os.Stderr = oldStderr + captured, _ := io.ReadAll(r) + stderr := string(captured) + + if strings.Contains(stderr, secret) { + t.Errorf("SECRET LEAKED in verbose output: %s", stderr) + } + if !strings.Contains(stderr, "****") { + t.Errorf("expected redacted value in output, got: %s", stderr) + } + if !strings.Contains(stderr, "[rest] GET") { + t.Errorf("expected verbose request line, got: %s", stderr) + } +} + +func TestRESTVerboseNoRedactWithoutSecrets(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte("ok")) + })) + defer srv.Close() + + p := NewREST(map[string]RESTToolDef{ + "test": { + Method: "GET", + BaseURL: srv.URL, + Path: "/public", + // No RedactSecrets — no vault refs + }, + }, nil) + p.Verbose = true + _ = p.Setup() + + oldStderr := os.Stderr + r, w, _ := os.Pipe() + os.Stderr = w + + _, _ = p.Execute("test", map[string]string{}) + + _ = w.Close() + os.Stderr = oldStderr + captured, _ := io.ReadAll(r) + stderr := string(captured) + + if !strings.Contains(stderr, "/public") { + t.Errorf("expected full URL in output without redaction, got: %s", stderr) + } +} + +func TestRESTVerboseRedactsSensitiveHeaders(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte("ok")) + })) + defer srv.Close() + + tests := []struct { + name string + headerName string + secret string + }{ + {"Authorization", "Authorization", "Bearer sk_live_12345"}, + {"X-Api-Key", "X-Api-Key", "apikey_secret_99"}, + {"X-Auth-Token", "X-Auth-Token", "authtoken_abc"}, + {"X-Secret-Id", "X-Secret-Id", "secret_id_xyz"}, + {"Cookie", "Cookie", "session=s3cr3t"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := NewREST(map[string]RESTToolDef{ + "test": { + Method: "GET", + BaseURL: srv.URL, + Path: "/", + Headers: map[string]string{tt.headerName: tt.secret}, + }, + }, nil) + p.Verbose = true + _ = p.Setup() + + oldStderr := os.Stderr + r, w, _ := os.Pipe() + os.Stderr = w + + _, _ = p.Execute("test", map[string]string{}) + + _ = w.Close() + os.Stderr = oldStderr + captured, _ := io.ReadAll(r) + stderr := string(captured) + + if strings.Contains(stderr, tt.secret) { + t.Errorf("SECRET LEAKED for header %s: %s", tt.headerName, stderr) + } + if !strings.Contains(stderr, tt.headerName+": ****") { + t.Errorf("expected %s: **** in output, got: %s", tt.headerName, stderr) + } + }) + } +} + +func TestRESTVerboseNonSensitiveHeadersNotRedacted(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte("ok")) + })) + defer srv.Close() + + p := NewREST(map[string]RESTToolDef{ + "test": { + Method: "GET", + BaseURL: srv.URL, + Path: "/", + Headers: map[string]string{ + "Accept": "application/json", + "Content-Type": "text/plain", + "X-Request-Id": "abc-123", + }, + }, + }, nil) + p.Verbose = true + _ = p.Setup() + + oldStderr := os.Stderr + r, w, _ := os.Pipe() + os.Stderr = w + + _, _ = p.Execute("test", map[string]string{}) + + _ = w.Close() + os.Stderr = oldStderr + captured, _ := io.ReadAll(r) + stderr := string(captured) + + if !strings.Contains(stderr, "application/json") { + t.Errorf("Accept header should not be redacted, got: %s", stderr) + } + if !strings.Contains(stderr, "text/plain") { + t.Errorf("Content-Type should not be redacted, got: %s", stderr) + } + if !strings.Contains(stderr, "abc-123") { + t.Errorf("X-Request-Id should not be redacted, got: %s", stderr) + } +} + +func TestIsSensitiveHeader(t *testing.T) { + sensitive := []string{ + "Authorization", "authorization", + "X-Api-Key", "x-api-key", + "X-Auth-Token", "X-AUTH-TOKEN", + "X-Secret", "x-secret-id", + "Cookie", "cookie", + "X-Access-Token", "Bearer-Token", + } + for _, h := range sensitive { + if !isSensitiveHeader(h) { + t.Errorf("expected %q to be sensitive", h) + } + } + + safe := []string{ + "Accept", "Content-Type", "Content-Length", + "X-Request-Id", "X-Correlation-Id", "User-Agent", + "Cache-Control", "Host", + } + for _, h := range safe { + if isSensitiveHeader(h) { + t.Errorf("expected %q to NOT be sensitive", h) + } } } diff --git a/src/internal/provider/workflow.go b/src/internal/provider/workflow.go index 36a0851..bd5324b 100644 --- a/src/internal/provider/workflow.go +++ b/src/internal/provider/workflow.go @@ -9,6 +9,7 @@ import ( "fmt" "os" "path/filepath" + "regexp" "strings" "time" @@ -82,6 +83,11 @@ func (p *WorkflowProvider) RegisterWorkflow(toolName string, steps []WorkflowSte p.steps[toolName] = steps } +// RemoveWorkflow removes a workflow's steps. +func (p *WorkflowProvider) RemoveWorkflow(toolName string) { + delete(p.steps, toolName) +} + func (p *WorkflowProvider) Setup() error { return nil } func (p *WorkflowProvider) Teardown() error { return nil } @@ -316,8 +322,15 @@ func (p *WorkflowProvider) ExecuteWithContext(ctx context.Context, toolName stri }, nil } +var exprPattern = regexp.MustCompile(`\{\{expr:(.+?)\}\}`) + func substituteVars(tmpl string, vars map[string]string) string { - result := tmpl + // First: evaluate {{expr:...}} patterns + result := exprPattern.ReplaceAllStringFunc(tmpl, func(match string) string { + expr := match[7 : len(match)-2] // strip {{expr: and }} + return EvalExpr(expr, vars) + }) + // Then: simple {{var}} substitution (backward compatible) for k, v := range vars { result = strings.ReplaceAll(result, "{{"+k+"}}", v) } diff --git a/src/internal/provider/workflow_test.go b/src/internal/provider/workflow_test.go index c43fa59..ffa6d7e 100644 --- a/src/internal/provider/workflow_test.go +++ b/src/internal/provider/workflow_test.go @@ -803,3 +803,115 @@ func TestWorkflowRequireWithParams(t *testing.T) { t.Error("expected skipped") } } + +func TestWorkflowExprSubstitution(t *testing.T) { + var capturedParams map[string]string + exec := &mockWorkflowExecutor{ + results: map[string]*Result{ + "fetch": {Output: `{"items":[{"name":"first"}]}`}, + }, + } + // Override to capture params + origExec := exec + wp := NewWorkflowProvider(¶mCapturingExecutor{ + executor: origExec, + capturedParams: &capturedParams, + captureFor: "process", + }, false) + wp.RegisterWorkflow("test", []WorkflowStep{ + {Tool: "fetch", Store: "data"}, + {Tool: "process", Params: map[string]string{ + "title": `{{expr:jsonpath(data, '$.items[0].name')}}`, + "upper": `{{expr:upper(input)}}`, + }}, + }) + _ = wp.Setup() + + result, err := wp.Execute("test", map[string]string{"input": "hello"}) + if err != nil { + t.Fatal(err) + } + if result.IsError() { + t.Fatal(result.Error) + } + if capturedParams["title"] != "first" { + t.Errorf("expected jsonpath result 'first', got %q", capturedParams["title"]) + } + if capturedParams["upper"] != "HELLO" { + t.Errorf("expected upper 'HELLO', got %q", capturedParams["upper"]) + } +} + +// paramCapturingExecutor wraps a mock executor and captures params for a specific tool. +type paramCapturingExecutor struct { + executor *mockWorkflowExecutor + capturedParams *map[string]string + captureFor string +} + +func (p *paramCapturingExecutor) ExecuteWithContext(ctx context.Context, toolName string, params map[string]string, iface string) (*Result, error) { + if toolName == p.captureFor { + captured := make(map[string]string) + for k, v := range params { + captured[k] = v + } + *p.capturedParams = captured + } + return p.executor.ExecuteWithContext(ctx, toolName, params, iface) +} + +func TestWorkflowExprNow(t *testing.T) { + var capturedParams map[string]string + exec := &mockWorkflowExecutor{} + wp := NewWorkflowProvider(¶mCapturingExecutor{ + executor: exec, + capturedParams: &capturedParams, + captureFor: "api_call", + }, false) + wp.RegisterWorkflow("test", []WorkflowStep{ + {Tool: "api_call", Params: map[string]string{ + "timestamp": `{{expr:now()}}`, + }}, + }) + _ = wp.Setup() + + _, err := wp.Execute("test", nil) + if err != nil { + t.Fatal(err) + } + if capturedParams["timestamp"] == "" || capturedParams["timestamp"] == "{{expr:now()}}" { + t.Errorf("expected resolved timestamp, got %q", capturedParams["timestamp"]) + } +} + +func TestWorkflowExprMixedWithVars(t *testing.T) { + var capturedParams map[string]string + exec := &mockWorkflowExecutor{ + results: map[string]*Result{ + "step1": {Output: "world"}, + }, + } + wp := NewWorkflowProvider(¶mCapturingExecutor{ + executor: exec, + capturedParams: &capturedParams, + captureFor: "step2", + }, false) + wp.RegisterWorkflow("test", []WorkflowStep{ + {Tool: "step1", Store: "greeting"}, + {Tool: "step2", Params: map[string]string{ + "msg": `hello {{greeting}} {{expr:upper(greeting)}}`, + }}, + }) + _ = wp.Setup() + + result, err := wp.Execute("test", nil) + if err != nil { + t.Fatal(err) + } + if result.IsError() { + t.Fatal(result.Error) + } + if capturedParams["msg"] != "hello world WORLD" { + t.Errorf("expected 'hello world WORLD', got %q", capturedParams["msg"]) + } +} diff --git a/src/internal/proxy/proxy.go b/src/internal/proxy/proxy.go index a080f8d..53920bf 100644 --- a/src/internal/proxy/proxy.go +++ b/src/internal/proxy/proxy.go @@ -18,6 +18,7 @@ import ( "github.com/factorly-dev/factorly/internal/provider" "github.com/factorly-dev/factorly/internal/registry" "github.com/factorly-dev/factorly/internal/shadow" + "github.com/factorly-dev/factorly/internal/vault" ) // Option configures a Proxy. @@ -28,11 +29,42 @@ func WithShadow(s *shadow.Policy) Option { return func(p *Proxy) { p.shadow = s } } +// WithOnCall sets a callback that fires after each tool call. +func WithOnCall(fn func(CallEvent)) Option { + return func(p *Proxy) { p.onCall = fn } +} + +// WithResolver sets a vault resolver for resolving {{backend:content}} +// patterns in param values at call time. +func WithResolver(r *vault.Resolver) Option { + return func(p *Proxy) { p.resolver = r } +} + +// SetOnCall sets the call event callback after proxy creation. +func (p *Proxy) SetOnCall(fn func(CallEvent)) { + p.onCall = fn +} + +// CallEvent is emitted after each tool call for live activity feeds. +type CallEvent struct { + Timestamp time.Time + Tool string + Params map[string]string + Status string // "success", "error", "blocked" + DurationMs int64 + ShadowAction string + AgentID string + Output string + Error string +} + type Proxy struct { registry *registry.Registry providers map[string]provider.Provider logger logger.Logger shadow *shadow.Policy + onCall func(CallEvent) + resolver *vault.Resolver } func New(reg *registry.Registry, providers map[string]provider.Provider, log logger.Logger, opts ...Option) *Proxy { @@ -52,6 +84,11 @@ func (p *Proxy) RegisterProvider(key string, prov provider.Provider) { p.providers[key] = prov } +// Provider returns a registered provider by key, or nil if not found. +func (p *Proxy) Provider(key string) provider.Provider { + return p.providers[key] +} + // Shadow returns the shadow policy, or nil if none is configured. func (p *Proxy) Shadow() *shadow.Policy { return p.shadow @@ -86,6 +123,17 @@ func (p *Proxy) ExecuteWithContext(ctx context.Context, toolName string, params } } + // Resolve {{backend:content}} patterns in param values (e.g., {{expr:now()}}) + if p.resolver != nil { + for k, v := range params { + if strings.Contains(v, "{{") { + if resolved, err := p.resolver.Resolve(v); err == nil { + params[k] = resolved + } + } + } + } + agentID := agent.AgentID(ctx) // Parameter validation and coercion @@ -104,6 +152,7 @@ func (p *Proxy) ExecuteWithContext(ctx context.Context, toolName string, params AgentID: agentID, } _ = p.logger.Log(entry) + p.emitCallEvent(entry) return nil, fmt.Errorf("parameter validation failed for %q: %s", toolName, strings.Join(validationResult.Errors, "; ")) } @@ -130,6 +179,7 @@ func (p *Proxy) ExecuteWithContext(ctx context.Context, toolName string, params } entry.AgentID = agentID _ = p.logger.Log(entry) + p.emitCallEvent(entry) return nil, err } } @@ -152,6 +202,7 @@ func (p *Proxy) ExecuteWithContext(ctx context.Context, toolName string, params AgentID: agentID, } _ = p.logger.Log(entry) + p.emitCallEvent(entry) return nil, err } } @@ -234,9 +285,32 @@ func (p *Proxy) ExecuteWithContext(ctx context.Context, toolName string, params fmt.Fprintf(os.Stderr, "warning: failed to log call: %v\n", logErr) } + p.emitCallEvent(entry) + return result, nil } +func (p *Proxy) emitCallEvent(entry *logger.Entry) { + if p.onCall == nil { + return + } + output := entry.Output + if len(output) > 500 { + output = output[:500] + "..." + } + p.onCall(CallEvent{ + Timestamp: entry.Timestamp, + Tool: entry.Tool, + Params: entry.Params, + Status: entry.Status, + DurationMs: entry.DurationMs, + ShadowAction: entry.ShadowAction, + AgentID: entry.AgentID, + Output: output, + Error: entry.Error, + }) +} + func filterParams(params map[string]string, keys []string) map[string]string { result := make(map[string]string, len(keys)) for _, k := range keys { diff --git a/src/internal/registry/registry.go b/src/internal/registry/registry.go index c60d8a4..6923bf4 100644 --- a/src/internal/registry/registry.go +++ b/src/internal/registry/registry.go @@ -29,6 +29,7 @@ type Tool struct { Name string Type string Description string + Hidden bool // hidden from MCP listing and tools list Parameters []Parameter ProviderKey string MaxOutput int @@ -62,6 +63,10 @@ func (r *Registry) Register(tool *Tool) { r.tools[tool.Name] = tool } +func (r *Registry) Unregister(name string) { + delete(r.tools, name) +} + func (r *Registry) Get(name string) (*Tool, error) { t, ok := r.tools[name] if !ok { @@ -70,6 +75,7 @@ func (r *Registry) Get(name string) (*Tool, error) { return t, nil } +// List returns all tools including hidden ones (for internal use like workflows). func (r *Registry) List() []*Tool { tools := make([]*Tool, 0, len(r.tools)) for _, t := range r.tools { @@ -80,3 +86,17 @@ func (r *Registry) List() []*Tool { }) return tools } + +// ListVisible returns only non-hidden tools (for MCP listing and CLI tools list). +func (r *Registry) ListVisible() []*Tool { + tools := make([]*Tool, 0, len(r.tools)) + for _, t := range r.tools { + if !t.Hidden { + tools = append(tools, t) + } + } + sort.Slice(tools, func(i, j int) bool { + return tools[i].Name < tools[j].Name + }) + return tools +} diff --git a/src/internal/server/mcp.go b/src/internal/server/mcp.go index 04dc196..c149f9c 100644 --- a/src/internal/server/mcp.go +++ b/src/internal/server/mcp.go @@ -104,7 +104,7 @@ func New(reg *registry.Registry, p *proxy.Proxy, agentReg ...*agent.Registry) *s server.WithHooks(hooks), ) - for _, tool := range reg.List() { + for _, tool := range reg.ListVisible() { // Hide denied tools from MCP clients if p.Shadow() != nil && p.Shadow().IsDenied(tool.Name) { continue diff --git a/src/internal/shadow/shadow.go b/src/internal/shadow/shadow.go index 12f276b..2acd787 100644 --- a/src/internal/shadow/shadow.go +++ b/src/internal/shadow/shadow.go @@ -64,6 +64,16 @@ func New(rules map[string]*Rule, confirmFn ConfirmFunc, rateStorePath string) *P } } +// SetRule adds or updates a shadow rule for a tool at runtime. +func (p *Policy) SetRule(toolName string, rule *Rule) { + p.rules[toolName] = rule +} + +// RemoveRule removes a shadow rule for a tool at runtime. +func (p *Policy) RemoveRule(toolName string) { + delete(p.rules, toolName) +} + // Check evaluates shadow rules for a tool call. Returns the action taken // and an error if the call should be blocked. func (p *Policy) Check(ctx context.Context, toolName string, params map[string]string, iface string) (Action, error) { diff --git a/src/internal/templates/telegram.go b/src/internal/templates/telegram.go index 1fe413a..e4e2a31 100644 --- a/src/internal/templates/telegram.go +++ b/src/internal/templates/telegram.go @@ -15,8 +15,8 @@ func Telegram() *Template { DisplayName: "Telegram", Description: "Bot messaging, channels, and notifications via phone", Category: "engineering", - AuthType: "bearer", - AuthGuide: "Create a bot with @BotFather on Telegram — it will give you the token", + AuthType: "vault", + AuthGuide: "Create a bot with @BotFather on Telegram, then run: factorly vault set TELEGRAM_BOT_TOKEN ", VaultKey: "TELEGRAM_BOT_TOKEN", YAML: telegramYAML, } diff --git a/src/internal/templates/templates_test.go b/src/internal/templates/templates_test.go index 2a524e1..bebb94b 100644 --- a/src/internal/templates/templates_test.go +++ b/src/internal/templates/templates_test.go @@ -61,7 +61,7 @@ func TestAllTemplatesYAMLValid(t *testing.T) { if tc.Method == "" { t.Errorf("template %q tool %s: empty method", tmpl.Name, name) } - if tc.Auth == nil { + if tc.Auth == nil && !strings.Contains(tc.Path, "{{vault:") { t.Errorf("template %q tool %s: missing auth", tmpl.Name, name) } case "cli": diff --git a/src/internal/templates/yaml/anthropic.yaml b/src/internal/templates/yaml/anthropic.yaml index fbd3236..c71b8d1 100644 --- a/src/internal/templates/yaml/anthropic.yaml +++ b/src/internal/templates/yaml/anthropic.yaml @@ -12,7 +12,7 @@ anthropic.messages: value: "{{vault:ANTHROPIC_API_KEY}}" parameters: - name: model - description: Model to use (e.g., claude-sonnet-4-20250514) + description: Model to use (e.g., claude-sonnet-4-6) type: string required: true in: body @@ -50,7 +50,7 @@ anthropic.ask: default: "You are a helpful assistant." - name: model description: Model to use - default: claude-sonnet-4-20250514 + default: claude-sonnet-4-6 - name: max_tokens description: Maximum tokens to generate default: "1024" diff --git a/src/internal/templates/yaml/google-calendar.yaml b/src/internal/templates/yaml/google-calendar.yaml index b433360..e226f6f 100644 --- a/src/internal/templates/yaml/google-calendar.yaml +++ b/src/internal/templates/yaml/google-calendar.yaml @@ -24,10 +24,12 @@ google-calendar.list_events: description: Maximum number of events to return in: query - name: singleEvents - description: Whether to expand recurring events (true/false) + description: Whether to expand recurring events into instances. Must be true when using orderBy=startTime. + default: "true" in: query - name: orderBy - description: Sort order (startTime or updated) + description: Sort order (startTime or updated). Requires singleEvents=true for startTime. + default: startTime in: query google-calendar.create_event: diff --git a/src/internal/templates/yaml/telegram.yaml b/src/internal/templates/yaml/telegram.yaml index 43efc01..7374c90 100644 --- a/src/internal/templates/yaml/telegram.yaml +++ b/src/internal/templates/yaml/telegram.yaml @@ -3,15 +3,8 @@ telegram.send_message: description: Send a text message to a chat base_url: "https://api.telegram.org" method: POST - path: "/bot{{token}}/sendMessage" - auth: - type: bearer - token: "{{vault:TELEGRAM_BOT_TOKEN}}" + path: "/bot{{vault:TELEGRAM_BOT_TOKEN}}/sendMessage" parameters: - - name: token - description: "Bot token (auto-filled from vault)" - required: true - in: path - name: chat_id description: "Chat ID or @channel_username" required: true @@ -31,15 +24,8 @@ telegram.get_updates: description: "Get incoming updates (messages, commands, etc.)" base_url: "https://api.telegram.org" method: POST - path: "/bot{{token}}/getUpdates" - auth: - type: bearer - token: "{{vault:TELEGRAM_BOT_TOKEN}}" + path: "/bot{{vault:TELEGRAM_BOT_TOKEN}}/getUpdates" parameters: - - name: token - description: Bot token - required: true - in: path - name: offset description: Update offset for pagination in: body @@ -52,15 +38,8 @@ telegram.send_document: description: Send a file to a chat base_url: "https://api.telegram.org" method: POST - path: "/bot{{token}}/sendDocument" - auth: - type: bearer - token: "{{vault:TELEGRAM_BOT_TOKEN}}" + path: "/bot{{vault:TELEGRAM_BOT_TOKEN}}/sendDocument" parameters: - - name: token - description: Bot token - required: true - in: path - name: chat_id description: "Chat ID or @channel_username" required: true @@ -80,15 +59,8 @@ telegram.get_chat: description: Get information about a chat base_url: "https://api.telegram.org" method: POST - path: "/bot{{token}}/getChat" - auth: - type: bearer - token: "{{vault:TELEGRAM_BOT_TOKEN}}" + path: "/bot{{vault:TELEGRAM_BOT_TOKEN}}/getChat" parameters: - - name: token - description: Bot token - required: true - in: path - name: chat_id description: "Chat ID or @channel_username" required: true @@ -99,15 +71,8 @@ telegram.send_photo: description: Send a photo to a chat base_url: "https://api.telegram.org" method: POST - path: "/bot{{token}}/sendPhoto" - auth: - type: bearer - token: "{{vault:TELEGRAM_BOT_TOKEN}}" + path: "/bot{{vault:TELEGRAM_BOT_TOKEN}}/sendPhoto" parameters: - - name: token - description: Bot token - required: true - in: path - name: chat_id description: "Chat ID or @channel_username" required: true @@ -127,12 +92,4 @@ telegram.get_me: description: Get information about the bot base_url: "https://api.telegram.org" method: POST - path: "/bot{{token}}/getMe" - auth: - type: bearer - token: "{{vault:TELEGRAM_BOT_TOKEN}}" - parameters: - - name: token - description: Bot token - required: true - in: path + path: "/bot{{vault:TELEGRAM_BOT_TOKEN}}/getMe" diff --git a/src/internal/ui/confirm.go b/src/internal/ui/confirm.go new file mode 100644 index 0000000..87aa540 --- /dev/null +++ b/src/internal/ui/confirm.go @@ -0,0 +1,195 @@ +// Copyright 2026 Jordan Sherer +// SPDX-License-Identifier: gpl + +package ui + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "sync" + "sync/atomic" +) + +// ConfirmRequest is a pending shadow confirmation prompt. +type ConfirmRequest struct { + ID string `json:"id"` + Tool string `json:"tool"` + Params map[string]string `json:"params"` + response chan bool +} + +// ConfirmBroker manages pending confirmation prompts between the shadow +// policy and the browser UI. +type ConfirmBroker struct { + mu sync.Mutex + pending map[string]*ConfirmRequest + subscribers map[chan struct{}]struct{} // SSE listeners + counter atomic.Int64 +} + +func NewConfirmBroker() *ConfirmBroker { + return &ConfirmBroker{ + pending: make(map[string]*ConfirmRequest), + subscribers: make(map[chan struct{}]struct{}), + } +} + +// Request creates a confirm prompt and blocks until the browser responds +// or the context is cancelled. Returns true if approved. +func (b *ConfirmBroker) Request(ctx context.Context, toolName string, params map[string]string) bool { + id := fmt.Sprintf("confirm-%d", b.counter.Add(1)) + req := &ConfirmRequest{ + ID: id, + Tool: toolName, + Params: params, + response: make(chan bool, 1), + } + + b.mu.Lock() + b.pending[id] = req + // Broadcast to all SSE listeners + for ch := range b.subscribers { + select { + case ch <- struct{}{}: + default: + } + } + b.mu.Unlock() + + defer func() { + b.mu.Lock() + delete(b.pending, id) + b.mu.Unlock() + }() + + select { + case approved := <-req.response: + return approved + case <-ctx.Done(): + return false + } +} + +// Respond resolves a pending confirmation. +func (b *ConfirmBroker) Respond(id string, approved bool) bool { + b.mu.Lock() + req, ok := b.pending[id] + b.mu.Unlock() + if !ok { + return false + } + select { + case req.response <- approved: + return true + default: + return false + } +} + +// Pending returns all pending confirmation requests. +func (b *ConfirmBroker) Pending() []*ConfirmRequest { + b.mu.Lock() + defer b.mu.Unlock() + var out []*ConfirmRequest + for _, r := range b.pending { + out = append(out, r) + } + return out +} + +// Subscribe returns a channel that is signaled when new confirm requests arrive. +// Caller must call Unsubscribe when done. +func (b *ConfirmBroker) Subscribe() chan struct{} { + ch := make(chan struct{}, 1) + b.mu.Lock() + b.subscribers[ch] = struct{}{} + b.mu.Unlock() + return ch +} + +// Unsubscribe removes a subscriber channel. +func (b *ConfirmBroker) Unsubscribe(ch chan struct{}) { + b.mu.Lock() + delete(b.subscribers, ch) + b.mu.Unlock() +} + +// --- HTTP handlers --- + +func (s *Server) handleConfirmPending(w http.ResponseWriter, r *http.Request) { + if s.confirmBroker == nil { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte("[]")) + return + } + pending := s.confirmBroker.Pending() + if pending == nil { + pending = []*ConfirmRequest{} + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(pending) +} + +func (s *Server) handleConfirmRespond(w http.ResponseWriter, r *http.Request) { + if s.confirmBroker == nil { + http.Error(w, "no confirm broker", http.StatusServiceUnavailable) + return + } + + id := r.PathValue("id") + action := r.PathValue("action") + + approved := action == "approve" + if ok := s.confirmBroker.Respond(id, approved); !ok { + http.Error(w, "confirm request not found or already resolved", http.StatusNotFound) + return + } + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + if approved { + fmt.Fprint(w, `Approved`) + } else { + fmt.Fprint(w, `Denied`) + } +} + +// handleConfirmSSE streams pending confirmations to the browser as SSE events. +func (s *Server) handleConfirmSSE(w http.ResponseWriter, r *http.Request) { + if s.confirmBroker == nil { + http.Error(w, "no confirm broker", http.StatusServiceUnavailable) + return + } + + flusher, ok := w.(http.Flusher) + if !ok { + http.Error(w, "streaming not supported", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + flusher.Flush() + + ctx := r.Context() + notify := s.confirmBroker.Subscribe() + defer s.confirmBroker.Unsubscribe(notify) + + for { + // Send current pending + pending := s.confirmBroker.Pending() + data, _ := json.Marshal(pending) + fmt.Fprintf(w, "data: %s\n\n", data) + flusher.Flush() + + // Wait for new request or disconnect + select { + case <-notify: + continue + case <-ctx.Done(): + return + } + } +} diff --git a/src/internal/ui/confirm_test.go b/src/internal/ui/confirm_test.go new file mode 100644 index 0000000..a41ffd2 --- /dev/null +++ b/src/internal/ui/confirm_test.go @@ -0,0 +1,108 @@ +// Copyright 2026 Jordan Sherer +// SPDX-License-Identifier: gpl + +package ui + +import ( + "context" + "testing" + "time" +) + +func TestConfirmBroker_ApproveFlow(t *testing.T) { + broker := NewConfirmBroker() + + // Simulate browser approving in background + go func() { + // Wait for request to appear + for { + pending := broker.Pending() + if len(pending) > 0 { + broker.Respond(pending[0].ID, true) + return + } + time.Sleep(5 * time.Millisecond) + } + }() + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + result := broker.Request(ctx, "dangerous.tool", map[string]string{"path": "/etc/hosts"}) + if !result { + t.Error("expected approval, got denial") + } +} + +func TestConfirmBroker_DenyFlow(t *testing.T) { + broker := NewConfirmBroker() + + go func() { + for { + pending := broker.Pending() + if len(pending) > 0 { + broker.Respond(pending[0].ID, false) + return + } + time.Sleep(5 * time.Millisecond) + } + }() + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + result := broker.Request(ctx, "dangerous.tool", nil) + if result { + t.Error("expected denial, got approval") + } +} + +func TestConfirmBroker_ContextCancelled(t *testing.T) { + broker := NewConfirmBroker() + + ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) + defer cancel() + + // No one responds — should timeout and return false + result := broker.Request(ctx, "slow.tool", nil) + if result { + t.Error("expected false on context cancellation") + } +} + +func TestConfirmBroker_RespondInvalidID(t *testing.T) { + broker := NewConfirmBroker() + + ok := broker.Respond("nonexistent", true) + if ok { + t.Error("responding to nonexistent ID should return false") + } +} + +func TestConfirmBroker_PendingList(t *testing.T) { + broker := NewConfirmBroker() + + // Start a request in background (will block) + go func() { + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + broker.Request(ctx, "tool.a", map[string]string{"x": "1"}) + }() + + // Wait for it to appear + time.Sleep(20 * time.Millisecond) + + pending := broker.Pending() + if len(pending) != 1 { + t.Fatalf("expected 1 pending, got %d", len(pending)) + } + if pending[0].Tool != "tool.a" { + t.Errorf("expected tool.a, got %s", pending[0].Tool) + } + if pending[0].Params["x"] != "1" { + t.Errorf("expected param x=1, got %v", pending[0].Params) + } + + // Clean up + broker.Respond(pending[0].ID, true) +} diff --git a/src/internal/ui/embed.go b/src/internal/ui/embed.go new file mode 100644 index 0000000..be52980 --- /dev/null +++ b/src/internal/ui/embed.go @@ -0,0 +1,12 @@ +// Copyright 2026 Jordan Sherer +// SPDX-License-Identifier: gpl + +package ui + +import "embed" + +//go:embed static/* +var staticFS embed.FS + +//go:embed templates/*.html templates/partials/*.html +var templatesFS embed.FS diff --git a/src/internal/ui/handlers_activity.go b/src/internal/ui/handlers_activity.go new file mode 100644 index 0000000..a2b701d --- /dev/null +++ b/src/internal/ui/handlers_activity.go @@ -0,0 +1,104 @@ +// Copyright 2026 Jordan Sherer +// SPDX-License-Identifier: gpl + +package ui + +import ( + "encoding/json" + "fmt" + "net/http" + "sync" + + "github.com/factorly-dev/factorly/internal/proxy" +) + +// ActivityBroadcaster fans out proxy CallEvents to all connected SSE clients. +type ActivityBroadcaster struct { + mu sync.Mutex + clients map[chan proxy.CallEvent]bool +} + +func NewActivityBroadcaster() *ActivityBroadcaster { + return &ActivityBroadcaster{ + clients: make(map[chan proxy.CallEvent]bool), + } +} + +// Broadcast sends an event to all connected clients. +func (b *ActivityBroadcaster) Broadcast(e proxy.CallEvent) { + b.mu.Lock() + defer b.mu.Unlock() + for ch := range b.clients { + select { + case ch <- e: + default: + // Client too slow, skip + } + } +} + +func (b *ActivityBroadcaster) subscribe() chan proxy.CallEvent { + ch := make(chan proxy.CallEvent, 32) + b.mu.Lock() + b.clients[ch] = true + b.mu.Unlock() + return ch +} + +func (b *ActivityBroadcaster) unsubscribe(ch chan proxy.CallEvent) { + b.mu.Lock() + delete(b.clients, ch) + b.mu.Unlock() + close(ch) +} + +func (s *Server) handleActivityStream(w http.ResponseWriter, r *http.Request) { + if s.activity == nil { + http.Error(w, "activity feed not available", http.StatusServiceUnavailable) + return + } + + flusher, ok := w.(http.Flusher) + if !ok { + http.Error(w, "streaming not supported", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + flusher.Flush() + + ch := s.activity.subscribe() + defer s.activity.unsubscribe(ch) + + ctx := r.Context() + for { + select { + case <-ctx.Done(): + return + case event, ok := <-ch: + if !ok { + return + } + data, _ := json.Marshal(map[string]any{ + "timestamp": event.Timestamp.Format("15:04:05"), + "tool": event.Tool, + "status": event.Status, + "duration_ms": event.DurationMs, + "shadow": event.ShadowAction, + "agent_id": event.AgentID, + "error": event.Error, + "output": event.Output, + "params": event.Params, + }) + fmt.Fprintf(w, "data: %s\n\n", data) + flusher.Flush() + } + } +} + +func (s *Server) handleActivity(w http.ResponseWriter, r *http.Request) { + // Activity is now a drawer in the layout — redirect to tools + http.Redirect(w, r, "/tools", http.StatusFound) +} diff --git a/src/internal/ui/handlers_auth.go b/src/internal/ui/handlers_auth.go new file mode 100644 index 0000000..c7bf02f --- /dev/null +++ b/src/internal/ui/handlers_auth.go @@ -0,0 +1,402 @@ +// Copyright 2026 Jordan Sherer +// SPDX-License-Identifier: gpl + +package ui + +import ( + "encoding/json" + "fmt" + "html" + "net/http" + "sort" + "strings" + "time" + + "github.com/factorly-dev/factorly/internal/config" + "github.com/factorly-dev/factorly/internal/oauth" +) + +type authProviderView struct { + Name string + Status string // "valid", "expired_refreshable", "expired", "missing" + StatusLabel string + StatusColor string // "green", "amber", "red" + Expiry string + Scopes []string + Tools []string + HasToken bool + ClientID string + ClientSecret string + AuthURL string + TokenURL string +} + +func (s *Server) handleAuth(w http.ResponseWriter, r *http.Request) { + providers := s.buildAuthProviderViews() + + s.render(w, "auth.html", map[string]any{ + "Title": "Auth", + "Nav": "auth", + "Providers": providers, + }) +} + +func (s *Server) handleAuthCreate(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + name := r.FormValue("name") + if name == "" { + http.Error(w, "name is required", http.StatusBadRequest) + return + } + + p := config.OAuthProviderConfig{ + ClientID: r.FormValue("client_id"), + ClientSecret: r.FormValue("client_secret"), + AuthURL: r.FormValue("auth_url"), + TokenURL: r.FormValue("token_url"), + } + if scopes := r.FormValue("scopes"); scopes != "" { + for _, s := range strings.Split(scopes, ",") { + s = strings.TrimSpace(s) + if s != "" { + p.Scopes = append(p.Scopes, s) + } + } + } + + if err := SaveOAuthProvider(s.cfgPath, name, p); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if s.cfg.OAuthProviders == nil { + s.cfg.OAuthProviders = make(map[string]config.OAuthProviderConfig) + } + s.cfg.OAuthProviders[name] = p + + http.Redirect(w, r, "/auth", http.StatusFound) +} + +func (s *Server) handleAuthUpdate(w http.ResponseWriter, r *http.Request) { + name := r.PathValue("provider") + if err := r.ParseForm(); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + p := config.OAuthProviderConfig{ + ClientID: r.FormValue("client_id"), + ClientSecret: r.FormValue("client_secret"), + AuthURL: r.FormValue("auth_url"), + TokenURL: r.FormValue("token_url"), + } + if scopes := r.FormValue("scopes"); scopes != "" { + for _, s := range strings.Split(scopes, ",") { + s = strings.TrimSpace(s) + if s != "" { + p.Scopes = append(p.Scopes, s) + } + } + } + + if err := SaveOAuthProvider(s.cfgPath, name, p); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + s.cfg.OAuthProviders[name] = p + + http.Redirect(w, r, "/auth", http.StatusFound) +} + +func (s *Server) handleAuthDelete(w http.ResponseWriter, r *http.Request) { + name := r.PathValue("provider") + + if err := DeleteOAuthProvider(s.cfgPath, name); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Delete token from vault + tokenKey := name + "_oauth" + if s.vault != nil { + _ = s.vault.Delete(tokenKey) + } + + delete(s.cfg.OAuthProviders, name) + + // Remove auth references from tools that used this provider + for toolName, tc := range s.cfg.Tools { + if tc.Auth != nil && tc.Auth.Type == "oauth" && tc.Auth.Provider == name { + tc.Auth = nil + s.cfg.Tools[toolName] = tc + _ = SaveTool(s.cfgPath, s.toolsDir, toolName, tc) + s.registerTool(toolName, tc) + } + } + + w.Header().Set("HX-Redirect", "/auth") + w.WriteHeader(http.StatusOK) +} + +func (s *Server) handleAuthLogout(w http.ResponseWriter, r *http.Request) { + providerName := r.PathValue("provider") + tokenKey := providerName + "_oauth" + + // Try to find the actual token key from config + if pCfg, ok := s.cfg.OAuthProviders[providerName]; ok { + _ = pCfg // default key is fine + } + for _, tc := range s.cfg.Tools { + if tc.Auth != nil && tc.Auth.Type == "oauth" { + key := config.OAuthTokenKey(tc.Auth) + if tc.Auth.Provider == providerName && key != "" { + tokenKey = key + break + } + } + } + + // Delete from whichever vault has it + if s.vault != nil { + _ = s.vault.Delete(tokenKey) + } + + // Return updated provider list + w.Header().Set("Content-Type", "text/html; charset=utf-8") + providers := s.buildAuthProviderViews() + s.renderAuthList(w, providers) +} + +func (s *Server) handleAuthRefresh(w http.ResponseWriter, r *http.Request) { + providerName := r.PathValue("provider") + + // Find the provider config and token key + pCfg, ok := s.cfg.OAuthProviders[providerName] + if !ok { + http.Error(w, "provider not found", http.StatusNotFound) + return + } + + tokenKey := providerName + "_oauth" + // Check tools for custom token key + for _, tc := range s.cfg.Tools { + if tc.Auth != nil && tc.Auth.Type == "oauth" && tc.Auth.Provider == providerName { + if key := config.OAuthTokenKey(tc.Auth); key != "" { + tokenKey = key + } + break + } + } + + if s.vault == nil { + http.Error(w, "vault not available", http.StatusServiceUnavailable) + return + } + + // Read current token + raw, err := s.vault.Get(tokenKey) + if err != nil { + http.Error(w, fmt.Sprintf("reading token: %v", err), http.StatusInternalServerError) + return + } + + var bundle oauth.TokenBundle + if err := json.Unmarshal([]byte(raw), &bundle); err != nil { + http.Error(w, "invalid token data", http.StatusInternalServerError) + return + } + + if bundle.RefreshToken == "" { + http.Error(w, "no refresh token available", http.StatusBadRequest) + return + } + + // Resolve vault refs in provider config + clientID := s.resolveRef(pCfg.ClientID) + clientSecret := s.resolveRef(pCfg.ClientSecret) + + providerCfg := oauth.ProviderConfig{ + ClientID: clientID, + ClientSecret: clientSecret, + TokenURL: pCfg.TokenURL, + } + + // Refresh + newBundle, err := oauth.RefreshAccessToken(r.Context(), providerCfg, bundle.RefreshToken) + if err != nil { + http.Error(w, fmt.Sprintf("refresh failed: %v", err), http.StatusInternalServerError) + return + } + + // Store refreshed token + data, _ := json.Marshal(newBundle) + if err := s.vault.Set(tokenKey, string(data)); err != nil { + http.Error(w, fmt.Sprintf("storing token: %v", err), http.StatusInternalServerError) + return + } + + // Return updated list + w.Header().Set("Content-Type", "text/html; charset=utf-8") + providers := s.buildAuthProviderViews() + s.renderAuthList(w, providers) +} + +func (s *Server) buildAuthProviderViews() []authProviderView { + var views []authProviderView + + // Collect from oauth_providers config + for name, pCfg := range s.cfg.OAuthProviders { + tokenKey := name + "_oauth" + view := authProviderView{ + Name: name, + Scopes: pCfg.Scopes, + Tools: s.findToolsUsingProvider(name), + ClientID: pCfg.ClientID, + ClientSecret: pCfg.ClientSecret, + AuthURL: pCfg.AuthURL, + TokenURL: pCfg.TokenURL, + } + s.populateTokenStatus(&view, tokenKey) + views = append(views, view) + } + + // Also check tools with inline OAuth that aren't in oauth_providers + seen := make(map[string]bool) + for _, v := range views { + seen[v.Name] = true + } + for _, tc := range s.cfg.Tools { + if tc.Auth != nil && tc.Auth.Type == "oauth" && tc.Auth.Provider != "" && !seen[tc.Auth.Provider] { + tokenKey := config.OAuthTokenKey(tc.Auth) + view := authProviderView{ + Name: tc.Auth.Provider, + Tools: s.findToolsUsingProvider(tc.Auth.Provider), + } + s.populateTokenStatus(&view, tokenKey) + views = append(views, view) + seen[tc.Auth.Provider] = true + } + } + + sort.Slice(views, func(i, j int) bool { return views[i].Name < views[j].Name }) + return views +} + +func (s *Server) populateTokenStatus(view *authProviderView, tokenKey string) { + if s.vault == nil || tokenKey == "" { + view.Status = "missing" + view.StatusLabel = "not authenticated" + view.StatusColor = "red" + return + } + + raw, err := s.vault.Get(tokenKey) + if err != nil || raw == "" { + view.Status = "missing" + view.StatusLabel = "not authenticated" + view.StatusColor = "red" + return + } + + var bundle oauth.TokenBundle + if json.Unmarshal([]byte(raw), &bundle) != nil { + view.Status = "missing" + view.StatusLabel = "invalid token" + view.StatusColor = "red" + return + } + + view.HasToken = true + + if bundle.Expiry.IsZero() { + view.Status = "valid" + view.StatusLabel = "valid (no expiry)" + view.StatusColor = "green" + return + } + + if bundle.IsExpired(0) { + if bundle.RefreshToken != "" { + view.Status = "expired_refreshable" + view.StatusLabel = "expired (will auto-refresh)" + view.StatusColor = "amber" + view.Expiry = relativeTime(bundle.Expiry) + } else { + view.Status = "expired" + view.StatusLabel = "expired" + view.StatusColor = "red" + view.Expiry = relativeTime(bundle.Expiry) + } + return + } + + view.Status = "valid" + remaining := time.Until(bundle.Expiry).Truncate(time.Minute) + view.StatusLabel = fmt.Sprintf("valid (expires in %s)", remaining) + view.StatusColor = "green" + view.Expiry = fmt.Sprintf("expires in %s", remaining) +} + +func (s *Server) findToolsUsingProvider(providerName string) []string { + var tools []string + for name, tc := range s.cfg.Tools { + if tc.Auth != nil && tc.Auth.Type == "oauth" && tc.Auth.Provider == providerName { + tools = append(tools, name) + } + } + sort.Strings(tools) + return tools +} + +func (s *Server) renderAuthList(w http.ResponseWriter, providers []authProviderView) { + if len(providers) == 0 { + fmt.Fprint(w, `
No OAuth providers configured.
`) + return + } + + for _, p := range providers { + icon := "✓" + if p.Status == "expired_refreshable" { + icon = "⟳" + } else if p.Status == "expired" || p.Status == "missing" { + icon = "✗" + } + + escName := html.EscapeString(p.Name) + escLabel := html.EscapeString(p.StatusLabel) + + fmt.Fprintf(w, `
+
+
+ %s + %s + %s +
+
`, + p.StatusColor, p.StatusColor, icon, escName, p.StatusColor, escLabel) + + if !p.HasToken { + fmt.Fprintf(w, `factorly auth login %s`, escName) + } + if p.HasToken { + fmt.Fprintf(w, ``, escName, escName) + } + + fmt.Fprint(w, `
`) + + // Tools using this provider + if len(p.Tools) > 0 { + fmt.Fprint(w, `
`) + for _, t := range p.Tools { + fmt.Fprintf(w, `%s`, html.EscapeString(t)) + } + fmt.Fprint(w, `
`) + } + + fmt.Fprint(w, `
`) + } +} diff --git a/src/internal/ui/handlers_history.go b/src/internal/ui/handlers_history.go new file mode 100644 index 0000000..b4caf1f --- /dev/null +++ b/src/internal/ui/handlers_history.go @@ -0,0 +1,141 @@ +// Copyright 2026 Jordan Sherer +// SPDX-License-Identifier: gpl + +package ui + +import ( + "bufio" + "encoding/json" + "fmt" + "net/http" + "os" + "strings" + "time" + + "github.com/factorly-dev/factorly/internal/logger" +) + +type historyEntry struct { + Timestamp string + TimestampRel string + Tool string + Interface string + Status string + DurationMs int64 + ShadowAction string + Output string + Error string + Params map[string]string + AgentID string +} + +func (s *Server) handleHistory(w http.ResponseWriter, r *http.Request) { + toolFilter := r.URL.Query().Get("tool") + statusFilter := r.URL.Query().Get("status") + + entries := readRecentLogs(100) + + // Apply filters + if toolFilter != "" || statusFilter != "" { + var filtered []historyEntry + for _, e := range entries { + if toolFilter != "" && !strings.Contains(e.Tool, toolFilter) { + continue + } + if statusFilter != "" && e.Status != statusFilter { + continue + } + filtered = append(filtered, e) + } + entries = filtered + } + + s.render(w, "history.html", map[string]any{ + "Title": "History", + "Nav": "history", + "Entries": entries, + "ToolFilter": toolFilter, + "StatusFilter": statusFilter, + }) +} + +func readRecentLogs(max int) []historyEntry { + path := logger.DefaultLogPath() + f, err := os.Open(path) + if err != nil { + return nil + } + defer f.Close() + + // Read all lines (we'll take the last `max`) + var lines []string + scanner := bufio.NewScanner(f) + scanner.Buffer(make([]byte, 0, 256*1024), 256*1024) + for scanner.Scan() { + lines = append(lines, scanner.Text()) + } + + // Take last `max` lines + start := 0 + if len(lines) > max { + start = len(lines) - max + } + lines = lines[start:] + + // Parse in reverse (most recent first) + entries := make([]historyEntry, 0, len(lines)) + for i := len(lines) - 1; i >= 0; i-- { + line := strings.TrimSpace(lines[i]) + if line == "" { + continue + } + var raw logger.Entry + if json.Unmarshal([]byte(line), &raw) != nil { + continue + } + entries = append(entries, historyEntry{ + Timestamp: raw.Timestamp.Format("2006-01-02 15:04:05"), + TimestampRel: relativeTime(raw.Timestamp), + Tool: raw.Tool, + Interface: raw.Interface, + Status: raw.Status, + DurationMs: raw.DurationMs, + ShadowAction: raw.ShadowAction, + Output: truncate(raw.Output, 200), + Error: raw.Error, + Params: raw.Params, + AgentID: raw.AgentID, + }) + } + + return entries +} + +func relativeTime(t time.Time) string { + d := time.Since(t) + switch { + case d < time.Minute: + return "just now" + case d < time.Hour: + m := int(d.Minutes()) + if m == 1 { + return "1m ago" + } + return fmt.Sprintf("%dm ago", m) + case d < 24*time.Hour: + h := int(d.Hours()) + if h == 1 { + return "1h ago" + } + return fmt.Sprintf("%dh ago", h) + default: + return t.Format("Jan 2") + } +} + +func truncate(s string, max int) string { + if len(s) > max { + return s[:max] + "..." + } + return s +} diff --git a/src/internal/ui/handlers_import.go b/src/internal/ui/handlers_import.go new file mode 100644 index 0000000..18d7b13 --- /dev/null +++ b/src/internal/ui/handlers_import.go @@ -0,0 +1,198 @@ +// Copyright 2026 Jordan Sherer +// SPDX-License-Identifier: gpl + +package ui + +import ( + "net/http" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/factorly-dev/factorly/internal/config" + "github.com/factorly-dev/factorly/internal/openapi" + "gopkg.in/yaml.v3" +) + +func (s *Server) handleImport(w http.ResponseWriter, r *http.Request) { + s.render(w, "import.html", map[string]any{ + "Title": "Import Tools", + "Nav": "tools", + }) +} + +func (s *Server) handleImportPreview(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + specURL := strings.TrimSpace(r.FormValue("spec_url")) + prefix := strings.TrimSpace(r.FormValue("prefix")) + + if specURL == "" { + s.renderPartial(w, "import_error", map[string]any{"Message": "Please provide a spec URL or path."}) + return + } + + tools, err := openapi.Generate(specURL, openapi.GenerateOpts{ + Prefix: prefix, + BaseDir: s.projectDir(), + }) + if err != nil { + s.renderPartial(w, "import_error", map[string]any{"Message": "Error: " + err.Error()}) + return + } + + if len(tools) == 0 { + s.renderPartial(w, "import_warning", map[string]any{"Message": "No endpoints found in the spec."}) + return + } + + // Build sorted tool list with method colors + type toolPreview struct { + Name string + Method string + MethodColor string + Description string + } + var names []string + for name := range tools { + names = append(names, name) + } + sort.Strings(names) + + var previews []toolPreview + for _, name := range names { + tc := tools[name] + method := tc.Method + if method == "" { + method = "GET" + } + color := "gray" + switch method { + case "GET": + color = "green" + case "POST": + color = "blue" + case "PUT": + color = "orange" + case "PATCH": + color = "yellow" + case "DELETE": + color = "red" + } + previews = append(previews, toolPreview{ + Name: name, + Method: method, + MethodColor: color, + Description: tc.Description, + }) + } + + s.renderPartial(w, "import_preview", map[string]any{ + "Count": len(tools), + "SpecURL": specURL, + "Prefix": prefix, + "Tools": previews, + }) +} + +func (s *Server) handleImportConfirm(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + specURL := strings.TrimSpace(r.FormValue("spec_url")) + prefix := strings.TrimSpace(r.FormValue("prefix")) + selectedTools := r.Form["tools"] + + if len(selectedTools) == 0 { + http.Error(w, "no tools selected", http.StatusBadRequest) + return + } + + // Re-generate from spec + tools, err := openapi.Generate(specURL, openapi.GenerateOpts{ + Prefix: prefix, + BaseDir: s.projectDir(), + }) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Filter to selected + selected := make(map[string]bool, len(selectedTools)) + for _, name := range selectedTools { + selected[name] = true + } + + // Save selected tools + if s.toolsDir != "" { + // Write as a single file in tools_dir + filtered := make(map[string]config.ToolConfig) + for name, tc := range tools { + if selected[name] { + filtered[name] = tc + } + } + out, err := yaml.Marshal(filtered) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + filename := prefix + if filename == "" { + filename = "imported" + } + safe, err := safePath(filename) + if err != nil { + http.Error(w, "invalid prefix: "+err.Error(), http.StatusBadRequest) + return + } + path := filepath.Join(s.toolsDir, safe+".yaml") + if err := writeFileCreate(path, out); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + // Update in-memory config and registry + for name, tc := range filtered { + s.cfg.Tools[name] = tc + s.registerTool(name, tc) + } + } else { + // Save each tool to main config + for name, tc := range tools { + if !selected[name] { + continue + } + if err := SaveTool(s.cfgPath, s.toolsDir, name, tc); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + s.cfg.Tools[name] = tc + s.registerTool(name, tc) + } + } + + http.Redirect(w, r, "/tools", http.StatusFound) +} + +// projectDir returns the directory containing the config file, used as the +// sandbox root for local file operations like spec import. +func (s *Server) projectDir() string { + if s.cfgPath != "" { + return filepath.Dir(s.cfgPath) + } + return "." +} + +func writeFileCreate(path string, data []byte) error { + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return err + } + return os.WriteFile(path, data, 0o644) +} diff --git a/src/internal/ui/handlers_templates.go b/src/internal/ui/handlers_templates.go new file mode 100644 index 0000000..92d8f58 --- /dev/null +++ b/src/internal/ui/handlers_templates.go @@ -0,0 +1,125 @@ +// Copyright 2026 Jordan Sherer +// SPDX-License-Identifier: gpl + +package ui + +import ( + "fmt" + "net/http" + "os" + "path/filepath" + + "github.com/factorly-dev/factorly/internal/config" + "github.com/factorly-dev/factorly/internal/templates" + "gopkg.in/yaml.v3" +) + +func (s *Server) handleTemplatesList(w http.ResponseWriter, r *http.Request) { + s.render(w, "templates.html", map[string]any{ + "Title": "Templates", + "Nav": "tools", + "Templates": templates.All(), + }) +} + +func (s *Server) handleTemplateDetail(w http.ResponseWriter, r *http.Request) { + name := r.PathValue("name") + tmpl := templates.Get(name) + if tmpl == nil { + http.NotFound(w, r) + return + } + + s.render(w, "template_detail.html", map[string]any{ + "Title": tmpl.DisplayName, + "Nav": "tools", + "Template": tmpl, + "ToolNames": tmpl.ToolNames(), + "YAML": tmpl.YAML, + }) +} + +func (s *Server) handleTemplateInstall(w http.ResponseWriter, r *http.Request) { + name := r.PathValue("name") + tmpl := templates.Get(name) + if tmpl == nil { + http.NotFound(w, r) + return + } + + // Get the full YAML for all tools in the template + toolYAML := tmpl.FilterYAML(tmpl.ToolNames()) + + // Write to tools_dir if configured, otherwise to main config + if s.toolsDir != "" { + if err := os.MkdirAll(s.toolsDir, 0o755); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + outPath := filepath.Join(s.toolsDir, name+".yaml") + if err := os.WriteFile(outPath, []byte(toolYAML), 0o644); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } else if s.cfgPath != "" { + // Merge into main config + var newTools map[string]any + if err := yaml.Unmarshal([]byte(toolYAML), &newTools); err != nil { + http.Error(w, fmt.Sprintf("parsing template YAML: %v", err), http.StatusInternalServerError) + return + } + + data, err := os.ReadFile(s.cfgPath) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + var raw map[string]any + if err := yaml.Unmarshal(data, &raw); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + tools, ok := raw["tools"].(map[string]any) + if !ok { + tools = make(map[string]any) + raw["tools"] = tools + } + for k, v := range newTools { + tools[k] = v + } + out, err := yaml.Marshal(raw) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + if err := os.WriteFile(s.cfgPath, out, 0o644); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } + + // Parse installed tools and register at runtime + var installedTools map[string]config.ToolConfig + if err := yaml.Unmarshal([]byte(toolYAML), &installedTools); err == nil { + for toolName, tc := range installedTools { + s.cfg.Tools[toolName] = tc + s.registerTool(toolName, tc) + } + } + + // Install OAuth provider if the template requires one + if oauthProviders := tmpl.ToOAuthProvider(); oauthProviders != nil { + for provName, provCfg := range oauthProviders { + if err := SaveOAuthProvider(s.cfgPath, provName, provCfg); err != nil { + http.Error(w, fmt.Sprintf("saving oauth provider: %v", err), http.StatusInternalServerError) + return + } + if s.cfg.OAuthProviders == nil { + s.cfg.OAuthProviders = make(map[string]config.OAuthProviderConfig) + } + s.cfg.OAuthProviders[provName] = provCfg + } + } + + http.Redirect(w, r, "/tools", http.StatusFound) +} diff --git a/src/internal/ui/handlers_test.go b/src/internal/ui/handlers_test.go new file mode 100644 index 0000000..e24cc83 --- /dev/null +++ b/src/internal/ui/handlers_test.go @@ -0,0 +1,1401 @@ +// Copyright 2026 Jordan Sherer +// SPDX-License-Identifier: gpl + +package ui + +import ( + "net/http" + "net/http/httptest" + "net/url" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/factorly-dev/factorly/internal/config" + "github.com/factorly-dev/factorly/internal/logger" + "github.com/factorly-dev/factorly/internal/provider" + "github.com/factorly-dev/factorly/internal/proxy" + "github.com/factorly-dev/factorly/internal/registry" + "github.com/factorly-dev/factorly/internal/vault" +) + +// mockVault implements vault.Backend for testing. +type mockVault struct { + data map[string]string +} + +func newMockVault() *mockVault { + return &mockVault{data: make(map[string]string)} +} + +func (m *mockVault) Get(key string) (string, error) { + v, ok := m.data[key] + if !ok { + return "", vault.ErrNotFound + } + return v, nil +} +func (m *mockVault) Set(key, value string) error { m.data[key] = value; return nil } +func (m *mockVault) Delete(key string) error { delete(m.data, key); return nil } +func (m *mockVault) List() ([]string, error) { + var keys []string + for k := range m.data { + keys = append(keys, k) + } + return keys, nil +} +func (m *mockVault) Close() error { return nil } + +// testServer creates a UI server with minimal config for testing. +func testServer(t *testing.T, cfg *config.Config) (*Server, string) { + t.Helper() + + dir := t.TempDir() + cfgPath := filepath.Join(dir, "factorly.yaml") + _ = os.WriteFile(cfgPath, []byte("tools: {}\n"), 0o644) + + if cfg == nil { + cfg = &config.Config{ + Tools: make(map[string]config.ToolConfig), + } + } + + srv, err := New(Options{ + Config: cfg, + CfgPath: cfgPath, + Vault: newMockVault(), + }) + if err != nil { + t.Fatal(err) + } + return srv, cfgPath +} + +// testServerWithProxy creates a UI server with registry and proxy for testing runtime registration. +func testServerWithProxy(t *testing.T, cfg *config.Config) (*Server, string) { + t.Helper() + + dir := t.TempDir() + cfgPath := filepath.Join(dir, "factorly.yaml") + _ = os.WriteFile(cfgPath, []byte("tools: {}\n"), 0o644) + + if cfg == nil { + cfg = &config.Config{ + Tools: make(map[string]config.ToolConfig), + } + } + + reg := registry.New() + providers := make(map[string]provider.Provider) + p := proxy.New(reg, providers, logger.NopLogger{}) + + srv, err := New(Options{ + Config: cfg, + CfgPath: cfgPath, + Vault: newMockVault(), + Registry: reg, + Proxy: p, + }) + if err != nil { + t.Fatal(err) + } + return srv, cfgPath +} + +// --- Tool handler tests --- + +func TestHandleToolsList(t *testing.T) { + cfg := &config.Config{ + Tools: map[string]config.ToolConfig{ + "echo": {Type: "cli", Command: "echo"}, + "api.fetch": {Type: "rest", Method: "GET", BaseURL: "https://example.com"}, + "deploy.all": {Type: "workflow", Steps: []config.StepConfig{{Tool: "echo"}}}, + }, + } + srv, _ := testServer(t, cfg) + + req := httptest.NewRequest("GET", "/tools", nil) + w := httptest.NewRecorder() + srv.mux.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", w.Code) + } + + body := w.Body.String() + // Should include non-workflow tools + if !strings.Contains(body, "echo") { + t.Error("expected 'echo' in tools list") + } + if !strings.Contains(body, "api.fetch") { + t.Error("expected 'api.fetch' in tools list") + } + // Should NOT include workflow tools + if strings.Contains(body, "deploy.all") { + t.Error("workflow 'deploy.all' should not appear in tools list") + } +} + +func TestHandleToolEdit_RedirectsWorkflows(t *testing.T) { + cfg := &config.Config{ + Tools: map[string]config.ToolConfig{ + "my.workflow": {Type: "workflow"}, + }, + } + srv, _ := testServer(t, cfg) + + req := httptest.NewRequest("GET", "/tools/my.workflow", nil) + w := httptest.NewRecorder() + srv.mux.ServeHTTP(w, req) + + if w.Code != http.StatusFound { + t.Fatalf("expected redirect 302, got %d", w.Code) + } + loc := w.Header().Get("Location") + if loc != "/workflows/my.workflow" { + t.Errorf("expected redirect to /workflows/my.workflow, got %s", loc) + } +} + +func TestHandleToolCreate(t *testing.T) { + srv, cfgPath := testServer(t, nil) + + form := url.Values{ + "name": {"test.tool"}, + "type": {"cli"}, + "description": {"A test tool"}, + "command": {"echo"}, + "args": {"hello world"}, + } + + req := httptest.NewRequest("POST", "/tools/_new", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + w := httptest.NewRecorder() + srv.mux.ServeHTTP(w, req) + + if w.Code != http.StatusFound { + t.Fatalf("expected redirect 302, got %d: %s", w.Code, w.Body.String()) + } + + // Verify in-memory config updated + tc, ok := srv.cfg.Tools["test.tool"] + if !ok { + t.Fatal("tool not found in config") + } + if tc.Type != "cli" { + t.Errorf("expected type cli, got %s", tc.Type) + } + if tc.Command != "echo" { + t.Errorf("expected command echo, got %s", tc.Command) + } + + // Verify written to disk + data, _ := os.ReadFile(cfgPath) + if !strings.Contains(string(data), "test.tool") { + t.Error("tool not written to config file") + } +} + +func TestHandleToolRename(t *testing.T) { + cfg := &config.Config{ + Tools: map[string]config.ToolConfig{ + "old.name": {Type: "cli", Command: "echo", Description: "original"}, + }, + } + srv, _ := testServer(t, cfg) + + form := url.Values{ + "rename": {"new.name"}, + "description": {"updated desc"}, + } + + req := httptest.NewRequest("POST", "/tools/old.name/rename", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + w := httptest.NewRecorder() + srv.mux.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) + } + + // Old name gone + if _, ok := srv.cfg.Tools["old.name"]; ok { + t.Error("old.name should be deleted") + } + // New name exists + tc, ok := srv.cfg.Tools["new.name"] + if !ok { + t.Fatal("new.name should exist") + } + if tc.Description != "updated desc" { + t.Errorf("expected 'updated desc', got %q", tc.Description) + } +} + +func TestHandleToolDelete(t *testing.T) { + cfg := &config.Config{ + Tools: map[string]config.ToolConfig{ + "doomed": {Type: "cli", Command: "rm"}, + }, + } + srv, _ := testServer(t, cfg) + + req := httptest.NewRequest("DELETE", "/tools/doomed", nil) + w := httptest.NewRecorder() + srv.mux.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", w.Code) + } + if _, ok := srv.cfg.Tools["doomed"]; ok { + t.Error("tool should have been deleted") + } +} + +// --- Workflow handler tests --- + +func TestHandleWorkflowsList(t *testing.T) { + cfg := &config.Config{ + Tools: map[string]config.ToolConfig{ + "echo": {Type: "cli", Command: "echo"}, + "git.deploy": {Type: "workflow", Steps: []config.StepConfig{{Tool: "echo"}, {Tool: "echo"}}}, + "git.release": {Type: "workflow", Steps: []config.StepConfig{{Tool: "echo"}}}, + }, + } + srv, _ := testServer(t, cfg) + + req := httptest.NewRequest("GET", "/workflows", nil) + w := httptest.NewRecorder() + srv.mux.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", w.Code) + } + + body := w.Body.String() + if !strings.Contains(body, "git.deploy") { + t.Error("expected git.deploy in workflows list") + } + if !strings.Contains(body, "git.release") { + t.Error("expected git.release in workflows list") + } + // Should not show non-workflow tools + if strings.Contains(body, ">echo<") { + t.Error("non-workflow tool should not appear") + } +} + +func TestHandleWorkflowCreate(t *testing.T) { + srv, _ := testServer(t, nil) + + form := url.Values{ + "name": {"test.flow"}, + "description": {"A test workflow"}, + } + + req := httptest.NewRequest("POST", "/workflows/_new", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + w := httptest.NewRecorder() + srv.mux.ServeHTTP(w, req) + + if w.Code != http.StatusFound { + t.Fatalf("expected 302, got %d: %s", w.Code, w.Body.String()) + } + + tc, ok := srv.cfg.Tools["test.flow"] + if !ok { + t.Fatal("workflow not found in config") + } + if tc.Type != "workflow" { + t.Errorf("expected type workflow, got %s", tc.Type) + } + if tc.Description != "A test workflow" { + t.Errorf("expected description, got %q", tc.Description) + } +} + +func TestHandleWorkflowDelete(t *testing.T) { + cfg := &config.Config{ + Tools: map[string]config.ToolConfig{ + "doomed.flow": {Type: "workflow"}, + }, + } + srv, _ := testServer(t, cfg) + + req := httptest.NewRequest("DELETE", "/workflows/doomed.flow", nil) + w := httptest.NewRecorder() + srv.mux.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", w.Code) + } + if _, ok := srv.cfg.Tools["doomed.flow"]; ok { + t.Error("workflow should have been deleted") + } +} + +// --- Auth handler tests --- + +func TestHandleAuthCreate(t *testing.T) { + srv, cfgPath := testServer(t, nil) + + form := url.Values{ + "name": {"github"}, + "client_id": {"abc123"}, + "client_secret": {"secret456"}, + "auth_url": {"https://github.com/login/oauth/authorize"}, + "token_url": {"https://github.com/login/oauth/access_token"}, + "scopes": {"repo, read:org"}, + } + + req := httptest.NewRequest("POST", "/auth/_new", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + w := httptest.NewRecorder() + srv.mux.ServeHTTP(w, req) + + if w.Code != http.StatusFound { + t.Fatalf("expected 302, got %d: %s", w.Code, w.Body.String()) + } + + p, ok := srv.cfg.OAuthProviders["github"] + if !ok { + t.Fatal("provider not found in config") + } + if p.ClientID != "abc123" { + t.Errorf("expected client_id abc123, got %s", p.ClientID) + } + if len(p.Scopes) != 2 || p.Scopes[0] != "repo" || p.Scopes[1] != "read:org" { + t.Errorf("unexpected scopes: %v", p.Scopes) + } + + // Verify on disk + data, _ := os.ReadFile(cfgPath) + if !strings.Contains(string(data), "github") { + t.Error("provider not written to config") + } +} + +func TestHandleAuthUpdate(t *testing.T) { + cfg := &config.Config{ + Tools: make(map[string]config.ToolConfig), + OAuthProviders: map[string]config.OAuthProviderConfig{ + "github": { + ClientID: "old_id", + ClientSecret: "old_secret", + AuthURL: "https://github.com/login/oauth/authorize", + TokenURL: "https://github.com/login/oauth/access_token", + Scopes: []string{"repo"}, + }, + }, + } + srv, _ := testServer(t, cfg) + + form := url.Values{ + "client_id": {"new_id"}, + "client_secret": {"new_secret"}, + "auth_url": {"https://github.com/login/oauth/authorize"}, + "token_url": {"https://github.com/login/oauth/access_token"}, + "scopes": {"repo, write:packages"}, + } + + req := httptest.NewRequest("POST", "/auth/github", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + w := httptest.NewRecorder() + srv.mux.ServeHTTP(w, req) + + if w.Code != http.StatusFound { + t.Fatalf("expected 302, got %d: %s", w.Code, w.Body.String()) + } + + p := srv.cfg.OAuthProviders["github"] + if p.ClientID != "new_id" { + t.Errorf("expected new_id, got %s", p.ClientID) + } + if len(p.Scopes) != 2 { + t.Errorf("expected 2 scopes, got %v", p.Scopes) + } +} + +func TestHandleAuthDelete(t *testing.T) { + v := newMockVault() + _ = v.Set("github_oauth", `{"access_token":"tok"}`) + + cfg := &config.Config{ + Tools: make(map[string]config.ToolConfig), + OAuthProviders: map[string]config.OAuthProviderConfig{ + "github": {ClientID: "x"}, + }, + } + + dir := t.TempDir() + cfgPath := filepath.Join(dir, "factorly.yaml") + _ = os.WriteFile(cfgPath, []byte("oauth_providers:\n github:\n client_id: x\n"), 0o644) + + srv, err := New(Options{ + Config: cfg, + CfgPath: cfgPath, + Vault: v, + }) + if err != nil { + t.Fatal(err) + } + + req := httptest.NewRequest("DELETE", "/auth/github", nil) + w := httptest.NewRecorder() + srv.mux.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", w.Code) + } + + // Provider removed from config + if _, ok := srv.cfg.OAuthProviders["github"]; ok { + t.Error("provider should be deleted") + } + + // Token removed from vault + if _, err := v.Get("github_oauth"); err == nil { + t.Error("token should be deleted from vault") + } +} + +// --- Vault handler tests --- + +func TestHandleVaultSetAndDelete(t *testing.T) { + v := newMockVault() + cfg := &config.Config{Tools: make(map[string]config.ToolConfig)} + + dir := t.TempDir() + cfgPath := filepath.Join(dir, "factorly.yaml") + _ = os.WriteFile(cfgPath, []byte("tools: {}\n"), 0o644) + + srv, err := New(Options{ + Config: cfg, + CfgPath: cfgPath, + Vault: v, + ProjectVault: v, + }) + if err != nil { + t.Fatal(err) + } + + // Set a key + form := url.Values{ + "key": {"MY_SECRET"}, + "value": {"supersecret"}, + "scope": {"project"}, + } + req := httptest.NewRequest("POST", "/vault", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + w := httptest.NewRecorder() + srv.mux.ServeHTTP(w, req) + + // Vault set returns 200 with htmx partial update + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) + } + + val, err := v.Get("MY_SECRET") + if err != nil { + t.Fatalf("key not found: %v", err) + } + if val != "supersecret" { + t.Errorf("expected 'supersecret', got %q", val) + } + + // Delete the key + req = httptest.NewRequest("DELETE", "/vault/MY_SECRET?scope=project", nil) + w = httptest.NewRecorder() + srv.mux.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", w.Code) + } +} + +// --- Tool save tests --- + +func TestHandleToolSave(t *testing.T) { + cfg := &config.Config{ + Tools: map[string]config.ToolConfig{ + "my.tool": {Type: "cli", Command: "echo", Description: "old"}, + }, + } + srv, _ := testServer(t, cfg) + + form := url.Values{ + "description": {"updated description"}, + "command": {"curl"}, + "args": {"-s https://example.com"}, + "param_name_0": {"url"}, + "param_type_0": {"string"}, + "param_default_0": {"https://example.com"}, + "param_desc_0": {"The URL to fetch"}, + } + + req := httptest.NewRequest("POST", "/tools/my.tool", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + w := httptest.NewRecorder() + srv.mux.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) + } + + tc := srv.cfg.Tools["my.tool"] + if tc.Description != "updated description" { + t.Errorf("description not updated: %q", tc.Description) + } + if tc.Command != "curl" { + t.Errorf("command not updated: %q", tc.Command) + } + if len(tc.Parameters) != 1 { + t.Fatalf("expected 1 param, got %d", len(tc.Parameters)) + } + if tc.Parameters[0].Name != "url" { + t.Errorf("param name: %q", tc.Parameters[0].Name) + } +} + +func TestHandleToolSave_WithShadow(t *testing.T) { + cfg := &config.Config{ + Tools: map[string]config.ToolConfig{ + "risky": {Type: "cli", Command: "rm"}, + }, + } + srv, _ := testServer(t, cfg) + + form := url.Values{ + "description": {"dangerous tool"}, + "command": {"rm"}, + "shadow_deny": {"path=/"}, + "shadow_confirm": {"on"}, + } + + req := httptest.NewRequest("POST", "/tools/risky", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + w := httptest.NewRecorder() + srv.mux.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) + } + + tc := srv.cfg.Tools["risky"] + if tc.Shadow == nil { + t.Fatal("shadow should be set") + } + if tc.Shadow.Confirm != true { + t.Error("shadow confirm should be true") + } + if len(tc.Shadow.Deny) != 1 || tc.Shadow.Deny[0] != "path=/" { + t.Errorf("shadow deny: %v", tc.Shadow.Deny) + } +} + +func TestHandleToolSave_WithHeaders(t *testing.T) { + cfg := &config.Config{ + Tools: map[string]config.ToolConfig{ + "api.test": {Type: "rest", Method: "GET", BaseURL: "https://example.com"}, + }, + } + srv, _ := testServer(t, cfg) + + form := url.Values{ + "description": {"test"}, + "method": {"GET"}, + "base_url": {"https://example.com"}, + "path": {"/test"}, + "header_key[]": {"Accept", "X-Custom"}, + "header_val[]": {"application/json", "myvalue"}, + } + + req := httptest.NewRequest("POST", "/tools/api.test", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + w := httptest.NewRecorder() + srv.mux.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) + } + + tc := srv.cfg.Tools["api.test"] + if len(tc.Headers) != 2 { + t.Fatalf("expected 2 headers, got %d: %v", len(tc.Headers), tc.Headers) + } + if tc.Headers["Accept"] != "application/json" { + t.Errorf("expected Accept header, got %v", tc.Headers) + } + if tc.Headers["X-Custom"] != "myvalue" { + t.Errorf("expected X-Custom header, got %v", tc.Headers) + } +} + +func TestHandleToolSave_HeadersCleared(t *testing.T) { + cfg := &config.Config{ + Tools: map[string]config.ToolConfig{ + "api.test": { + Type: "rest", + Method: "GET", + BaseURL: "https://example.com", + Headers: map[string]string{"Old": "value"}, + }, + }, + } + srv, _ := testServer(t, cfg) + + // Save with no headers + form := url.Values{ + "description": {"test"}, + "method": {"GET"}, + "base_url": {"https://example.com"}, + "path": {"/test"}, + } + + req := httptest.NewRequest("POST", "/tools/api.test", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + w := httptest.NewRecorder() + srv.mux.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) + } + + tc := srv.cfg.Tools["api.test"] + if tc.Headers != nil { + t.Errorf("expected nil headers after clearing, got %v", tc.Headers) + } +} + +func TestHandleToolSave_NotFound(t *testing.T) { + srv, _ := testServer(t, nil) + + form := url.Values{"description": {"x"}} + req := httptest.NewRequest("POST", "/tools/nonexistent", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + w := httptest.NewRecorder() + srv.mux.ServeHTTP(w, req) + + if w.Code != http.StatusNotFound { + t.Fatalf("expected 404, got %d", w.Code) + } +} + +func TestHandleToolCreate_MCP(t *testing.T) { + srv, _ := testServer(t, nil) + + form := url.Values{ + "name": {"mcp.github"}, + "type": {"mcp"}, + "command": {"npx"}, + "args": {"-y @modelcontextprotocol/server-github"}, + "url": {"http://localhost:8080/mcp"}, + } + + req := httptest.NewRequest("POST", "/tools/_new", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + w := httptest.NewRecorder() + srv.mux.ServeHTTP(w, req) + + if w.Code != http.StatusFound { + t.Fatalf("expected 302, got %d: %s", w.Code, w.Body.String()) + } + + tc := srv.cfg.Tools["mcp.github"] + if tc.Type != "mcp" { + t.Errorf("expected type mcp, got %s", tc.Type) + } + if tc.Command != "npx" { + t.Errorf("expected command npx, got %s", tc.Command) + } + if tc.URL != "http://localhost:8080/mcp" { + t.Errorf("expected URL, got %s", tc.URL) + } +} + +// --- Workflow save tests --- + +func TestHandleWorkflowSave(t *testing.T) { + cfg := &config.Config{ + Tools: map[string]config.ToolConfig{ + "echo": {Type: "cli", Command: "echo"}, + "my.flow": {Type: "workflow", Steps: []config.StepConfig{{Tool: "echo"}}}, + }, + } + srv, _ := testServer(t, cfg) + + form := url.Values{ + "description": {"updated flow"}, + "step_tool_0": {"echo"}, + "step_store_0": {"result"}, + "step_if_0": {"result != ''"}, + "step_tool_1": {"echo"}, + "step_store_1": {"final"}, + } + + req := httptest.NewRequest("POST", "/workflows/my.flow/save", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + w := httptest.NewRecorder() + srv.mux.ServeHTTP(w, req) + + if w.Code != http.StatusFound { + t.Fatalf("expected 302, got %d: %s", w.Code, w.Body.String()) + } + + tc := srv.cfg.Tools["my.flow"] + if tc.Description != "updated flow" { + t.Errorf("description not updated: %q", tc.Description) + } + if len(tc.Steps) != 2 { + t.Fatalf("expected 2 steps, got %d", len(tc.Steps)) + } + if tc.Steps[0].Store != "result" { + t.Errorf("step 0 store: %q", tc.Steps[0].Store) + } + if tc.Steps[0].If != "result != ''" { + t.Errorf("step 0 if: %q", tc.Steps[0].If) + } + if tc.Steps[1].Store != "final" { + t.Errorf("step 1 store: %q", tc.Steps[1].Store) + } +} + +func TestHandleWorkflowSave_WithParams(t *testing.T) { + cfg := &config.Config{ + Tools: map[string]config.ToolConfig{ + "echo": {Type: "cli", Command: "echo"}, + "my.flow": {Type: "workflow"}, + }, + } + srv, _ := testServer(t, cfg) + + form := url.Values{ + "description": {"flow with params"}, + "step_tool_0": {"echo"}, + "step_store_0": {""}, + "step_if_0": {""}, + "step_require_0": {""}, + "step_param_key_0[]": {"message", "count"}, + "step_param_val_0[]": {"hello", "5"}, + } + + req := httptest.NewRequest("POST", "/workflows/my.flow/save", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + w := httptest.NewRecorder() + srv.mux.ServeHTTP(w, req) + + if w.Code != http.StatusFound { + t.Fatalf("expected 302, got %d: %s", w.Code, w.Body.String()) + } + + tc := srv.cfg.Tools["my.flow"] + if len(tc.Steps) != 1 { + t.Fatalf("expected 1 step, got %d", len(tc.Steps)) + } + if tc.Steps[0].Params["message"] != "hello" { + t.Errorf("param message: %q", tc.Steps[0].Params["message"]) + } + if tc.Steps[0].Params["count"] != "5" { + t.Errorf("param count: %q", tc.Steps[0].Params["count"]) + } +} + +func TestHandleWorkflowRename(t *testing.T) { + cfg := &config.Config{ + Tools: map[string]config.ToolConfig{ + "old.flow": {Type: "workflow", Description: "original"}, + }, + } + srv, _ := testServer(t, cfg) + + form := url.Values{ + "rename": {"new.flow"}, + "description": {"renamed"}, + } + + req := httptest.NewRequest("POST", "/workflows/old.flow/rename", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + w := httptest.NewRecorder() + srv.mux.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) + } + + if _, ok := srv.cfg.Tools["old.flow"]; ok { + t.Error("old.flow should be gone") + } + tc, ok := srv.cfg.Tools["new.flow"] + if !ok { + t.Fatal("new.flow should exist") + } + if tc.Description != "renamed" { + t.Errorf("expected 'renamed', got %q", tc.Description) + } +} + +func TestHandleToolRename_SameName(t *testing.T) { + cfg := &config.Config{ + Tools: map[string]config.ToolConfig{ + "my.tool": {Type: "cli", Command: "echo", Description: "old"}, + }, + } + srv, _ := testServer(t, cfg) + + // Rename to same name — should just update description + form := url.Values{ + "rename": {"my.tool"}, + "description": {"new desc"}, + } + + req := httptest.NewRequest("POST", "/tools/my.tool/rename", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + w := httptest.NewRecorder() + srv.mux.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) + } + + tc := srv.cfg.Tools["my.tool"] + if tc.Description != "new desc" { + t.Errorf("description should be updated: %q", tc.Description) + } +} + +// --- Auth edge cases --- + +func TestHandleAuthCreate_EmptyScopes(t *testing.T) { + srv, _ := testServer(t, nil) + + form := url.Values{ + "name": {"minimal"}, + "client_id": {"id"}, + "client_secret": {"secret"}, + "auth_url": {"https://auth.example.com"}, + "token_url": {"https://token.example.com"}, + "scopes": {""}, + } + + req := httptest.NewRequest("POST", "/auth/_new", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + w := httptest.NewRecorder() + srv.mux.ServeHTTP(w, req) + + if w.Code != http.StatusFound { + t.Fatalf("expected 302, got %d: %s", w.Code, w.Body.String()) + } + + p := srv.cfg.OAuthProviders["minimal"] + if len(p.Scopes) != 0 { + t.Errorf("expected no scopes, got %v", p.Scopes) + } +} + +func TestHandleAuthCreate_MissingName(t *testing.T) { + srv, _ := testServer(t, nil) + + form := url.Values{ + "name": {""}, + "client_id": {"id"}, + } + + req := httptest.NewRequest("POST", "/auth/_new", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + w := httptest.NewRecorder() + srv.mux.ServeHTTP(w, req) + + if w.Code != http.StatusBadRequest { + t.Fatalf("expected 400, got %d", w.Code) + } +} + +// --- Import handler tests --- + +func TestHandleImportPreview_EmptyURL(t *testing.T) { + srv, _ := testServer(t, nil) + + form := url.Values{"spec_url": {""}, "prefix": {""}} + req := httptest.NewRequest("POST", "/tools/import/preview", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + w := httptest.NewRecorder() + srv.mux.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", w.Code) + } + if !strings.Contains(w.Body.String(), "Please provide") { + t.Error("expected error message for empty URL") + } +} + +func TestHandleImportPreview_InvalidURL(t *testing.T) { + srv, _ := testServer(t, nil) + + form := url.Values{"spec_url": {"/nonexistent/path/spec.yaml"}, "prefix": {""}} + req := httptest.NewRequest("POST", "/tools/import/preview", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + w := httptest.NewRecorder() + srv.mux.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", w.Code) + } + if !strings.Contains(w.Body.String(), "Error:") { + t.Error("expected error message for invalid path") + } +} + +func TestHandleImportConfirm_NoSelection(t *testing.T) { + srv, _ := testServer(t, nil) + + form := url.Values{"spec_url": {"http://example.com/spec.json"}, "prefix": {"test"}} + req := httptest.NewRequest("POST", "/tools/import/confirm", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + w := httptest.NewRecorder() + srv.mux.ServeHTTP(w, req) + + if w.Code != http.StatusBadRequest { + t.Fatalf("expected 400, got %d", w.Code) + } +} + +func TestHandleImportPage(t *testing.T) { + srv, _ := testServer(t, nil) + + req := httptest.NewRequest("GET", "/tools/import", nil) + w := httptest.NewRecorder() + srv.mux.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", w.Code) + } + if !strings.Contains(w.Body.String(), "OpenAPI") { + t.Error("expected OpenAPI mention on import page") + } +} + +// --- Runtime registration tests --- + +func TestToolCreate_RegistersInRegistryAndProvider(t *testing.T) { + srv, _ := testServerWithProxy(t, nil) + + form := url.Values{ + "name": {"test.echo"}, + "type": {"cli"}, + "command": {"echo"}, + "args": {"hello"}, + } + + req := httptest.NewRequest("POST", "/tools/_new", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + w := httptest.NewRecorder() + srv.mux.ServeHTTP(w, req) + + if w.Code != http.StatusFound { + t.Fatalf("expected 302, got %d: %s", w.Code, w.Body.String()) + } + + // Verify registered in registry + tool, err := srv.registry.Get("test.echo") + if err != nil { + t.Fatalf("tool not in registry: %v", err) + } + if tool.Type != "cli" { + t.Errorf("expected type cli, got %s", tool.Type) + } + + // Verify CLI provider was created and has the tool + prov := srv.proxy.Provider("cli") + if prov == nil { + t.Fatal("cli provider should exist") + } + cp, ok := prov.(*provider.CLIProvider) + if !ok { + t.Fatal("expected *provider.CLIProvider") + } + // Execute to prove it's registered (will fail since echo isn't a real command in test, but shouldn't be "not found") + _ = cp +} + +func TestToolCreate_REST_RegistersProvider(t *testing.T) { + srv, _ := testServerWithProxy(t, nil) + + form := url.Values{ + "name": {"api.test"}, + "type": {"rest"}, + "method": {"GET"}, + "base_url": {"https://httpbin.org"}, + "path": {"/get"}, + } + + req := httptest.NewRequest("POST", "/tools/_new", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + w := httptest.NewRecorder() + srv.mux.ServeHTTP(w, req) + + if w.Code != http.StatusFound { + t.Fatalf("expected 302, got %d: %s", w.Code, w.Body.String()) + } + + // Verify REST provider was created + prov := srv.proxy.Provider("rest") + if prov == nil { + t.Fatal("rest provider should exist") + } + if _, ok := prov.(*provider.RESTProvider); !ok { + t.Fatal("expected *provider.RESTProvider") + } + + // Verify in registry + if _, err := srv.registry.Get("api.test"); err != nil { + t.Fatalf("tool not in registry: %v", err) + } +} + +func TestToolDelete_UnregistersFromRegistryAndProvider(t *testing.T) { + cfg := &config.Config{ + Tools: map[string]config.ToolConfig{ + "doomed": {Type: "cli", Command: "echo"}, + }, + } + srv, _ := testServerWithProxy(t, cfg) + + // First register it + srv.registerTool("doomed", cfg.Tools["doomed"]) + + // Verify it's registered + if _, err := srv.registry.Get("doomed"); err != nil { + t.Fatal("tool should be in registry before delete") + } + + req := httptest.NewRequest("DELETE", "/tools/doomed", nil) + w := httptest.NewRecorder() + srv.mux.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", w.Code) + } + + // Verify removed from registry + if _, err := srv.registry.Get("doomed"); err == nil { + t.Error("tool should be removed from registry") + } +} + +func TestToolRename_UpdatesRegistryAndProvider(t *testing.T) { + cfg := &config.Config{ + Tools: map[string]config.ToolConfig{ + "old.tool": {Type: "cli", Command: "echo"}, + }, + } + srv, _ := testServerWithProxy(t, cfg) + srv.registerTool("old.tool", cfg.Tools["old.tool"]) + + form := url.Values{ + "rename": {"new.tool"}, + "description": {"renamed"}, + } + + req := httptest.NewRequest("POST", "/tools/old.tool/rename", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + w := httptest.NewRecorder() + srv.mux.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) + } + + // Old name gone from registry + if _, err := srv.registry.Get("old.tool"); err == nil { + t.Error("old.tool should be removed from registry") + } + // New name in registry + if _, err := srv.registry.Get("new.tool"); err != nil { + t.Errorf("new.tool should be in registry: %v", err) + } +} + +func TestToolSave_UpdatesProvider(t *testing.T) { + cfg := &config.Config{ + Tools: map[string]config.ToolConfig{ + "my.tool": {Type: "rest", Method: "GET", BaseURL: "https://old.example.com", Path: "/old"}, + }, + } + srv, _ := testServerWithProxy(t, cfg) + srv.registerTool("my.tool", cfg.Tools["my.tool"]) + + form := url.Values{ + "description": {"updated"}, + "method": {"POST"}, + "base_url": {"https://new.example.com"}, + "path": {"/new"}, + } + + req := httptest.NewRequest("POST", "/tools/my.tool", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + w := httptest.NewRecorder() + srv.mux.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) + } + + // Verify registry updated + tool, _ := srv.registry.Get("my.tool") + if tool.Description != "updated" { + t.Errorf("registry not updated: %q", tool.Description) + } +} + +// --- Integration: create → execute flow --- + +func TestIntegration_CreateRESTTool_ThenTryIt(t *testing.T) { + // Start a mock API server + mockAPI := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"status":"ok","path":"` + r.URL.Path + `"}`)) + })) + defer mockAPI.Close() + + srv, _ := testServerWithProxy(t, nil) + + // 1. Create a REST tool via POST + form := url.Values{ + "name": {"test.api"}, + "type": {"rest"}, + "method": {"GET"}, + "base_url": {mockAPI.URL}, + "path": {"/items"}, + } + req := httptest.NewRequest("POST", "/tools/_new", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + w := httptest.NewRecorder() + srv.mux.ServeHTTP(w, req) + + if w.Code != http.StatusFound { + t.Fatalf("create: expected 302, got %d: %s", w.Code, w.Body.String()) + } + + // 2. Verify it's in registry + if _, err := srv.registry.Get("test.api"); err != nil { + t.Fatalf("tool not in registry: %v", err) + } + + // 3. Verify it's in the REST provider + prov := srv.proxy.Provider("rest") + if prov == nil { + t.Fatal("rest provider should exist") + } + + // 4. Try It — execute via the proxy + tryForm := url.Values{} + req = httptest.NewRequest("POST", "/tools/test.api/try", strings.NewReader(tryForm.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + w = httptest.NewRecorder() + srv.mux.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("try: expected 200, got %d: %s", w.Code, w.Body.String()) + } + + body := w.Body.String() + if !strings.Contains(body, "ok") { + t.Errorf("try result should contain API response, got: %s", body) + } +} + +func TestIntegration_CreateRESTTool_WithPathParams(t *testing.T) { + var receivedPath string + mockAPI := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + receivedPath = r.URL.Path + _, _ = w.Write([]byte(`{"path":"` + r.URL.Path + `"}`)) + })) + defer mockAPI.Close() + + cfg := &config.Config{ + Tools: make(map[string]config.ToolConfig), + } + srv, _ := testServerWithProxy(t, cfg) + + // Manually add a tool with path params (simulating import) + tc := config.ToolConfig{ + Type: "rest", + Method: "GET", + BaseURL: mockAPI.URL, + Path: "/repos/{{owner}}/{{repo}}", + Parameters: []config.ParamConfig{ + {Name: "owner", Required: true}, + {Name: "repo", Required: true}, + }, + } + srv.cfg.Tools["repos.get"] = tc + srv.registerTool("repos.get", tc) + + // Try It with params + tryForm := url.Values{ + "param_owner": {"factorly-dev"}, + "param_repo": {"factorly"}, + } + req := httptest.NewRequest("POST", "/tools/repos.get/try", strings.NewReader(tryForm.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + w := httptest.NewRecorder() + srv.mux.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("try: expected 200, got %d: %s", w.Code, w.Body.String()) + } + + if receivedPath != "/repos/factorly-dev/factorly" { + t.Errorf("expected /repos/factorly-dev/factorly, got %s", receivedPath) + } +} + +func TestIntegration_CreateCLITool_ThenTryIt(t *testing.T) { + srv, _ := testServerWithProxy(t, nil) + + form := url.Values{ + "name": {"test.echo"}, + "type": {"cli"}, + "command": {"echo"}, + "args": {"hello world"}, + } + req := httptest.NewRequest("POST", "/tools/_new", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + w := httptest.NewRecorder() + srv.mux.ServeHTTP(w, req) + + if w.Code != http.StatusFound { + t.Fatalf("create: expected 302, got %d", w.Code) + } + + // Try It + req = httptest.NewRequest("POST", "/tools/test.echo/try", strings.NewReader(url.Values{}.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + w = httptest.NewRecorder() + srv.mux.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("try: expected 200, got %d: %s", w.Code, w.Body.String()) + } + + body := w.Body.String() + if !strings.Contains(body, "hello world") { + t.Errorf("try result should contain 'hello world', got: %s", body) + } +} + +func TestIntegration_SaveToolUpdatesProvider(t *testing.T) { + // Create a mock server that echoes the path + mockAPI := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte(r.URL.Path)) + })) + defer mockAPI.Close() + + cfg := &config.Config{ + Tools: map[string]config.ToolConfig{ + "my.api": {Type: "rest", Method: "GET", BaseURL: mockAPI.URL, Path: "/old"}, + }, + } + srv, _ := testServerWithProxy(t, cfg) + srv.registerTool("my.api", cfg.Tools["my.api"]) + + // Save with new path + form := url.Values{ + "description": {"updated"}, + "method": {"GET"}, + "base_url": {mockAPI.URL}, + "path": {"/new"}, + } + req := httptest.NewRequest("POST", "/tools/my.api", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + w := httptest.NewRecorder() + srv.mux.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("save: expected 200, got %d: %s", w.Code, w.Body.String()) + } + + // Try It — should hit /new, not /old + req = httptest.NewRequest("POST", "/tools/my.api/try", strings.NewReader(url.Values{}.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + w = httptest.NewRecorder() + srv.mux.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("try: expected 200, got %d: %s", w.Code, w.Body.String()) + } + + body := w.Body.String() + if !strings.Contains(body, "/new") { + t.Errorf("expected /new in response, got: %s", body) + } + if strings.Contains(body, "/old") { + t.Errorf("should not contain /old, got: %s", body) + } +} + +func TestIntegration_VaultRefResolution(t *testing.T) { + v := newMockVault() + _ = v.Set("API_KEY", "secret-123") + + cfg := &config.Config{ + Tools: make(map[string]config.ToolConfig), + } + + dir := t.TempDir() + cfgPath := filepath.Join(dir, "factorly.yaml") + _ = os.WriteFile(cfgPath, []byte("tools: {}\n"), 0o644) + + reg := registry.New() + providers := make(map[string]provider.Provider) + p := proxy.New(reg, providers, logger.NopLogger{}) + + resolver := vault.NewResolver() + resolver.Register("vault", v) + + srv, err := New(Options{ + Config: cfg, + CfgPath: cfgPath, + Vault: v, + Resolver: resolver, + Registry: reg, + Proxy: p, + }) + if err != nil { + t.Fatal(err) + } + + // Register a CLI tool with a vault ref in the command + tc := config.ToolConfig{ + Type: "cli", + Command: "echo", + Args: []string{"{{vault:API_KEY}}"}, + } + srv.cfg.Tools["test.vault"] = tc + srv.registerTool("test.vault", tc) + + // Verify vault key is tracked on the registry tool + tool, _ := srv.registry.Get("test.vault") + if len(tool.VaultKeys) == 0 { + t.Fatal("expected vault keys to be tracked") + } + found := false + for _, k := range tool.VaultKeys { + if k == "vault:API_KEY" { + found = true + } + } + if !found { + t.Errorf("expected 'vault:API_KEY' in vault keys, got %v", tool.VaultKeys) + } +} + +// --- Template rendering tests --- + +func TestAllTemplatesRender(t *testing.T) { + // Just verify that creating the server (which parses all templates) succeeds + _, _ = testServer(t, nil) + // If we get here without panic/fatal, all templates parse correctly +} diff --git a/src/internal/ui/handlers_tools.go b/src/internal/ui/handlers_tools.go new file mode 100644 index 0000000..8200efe --- /dev/null +++ b/src/internal/ui/handlers_tools.go @@ -0,0 +1,797 @@ +// Copyright 2026 Jordan Sherer +// SPDX-License-Identifier: gpl + +package ui + +import ( + "context" + "encoding/json" + "fmt" + "html/template" + "net/http" + "sort" + "strconv" + "strings" + "time" + + "github.com/factorly-dev/factorly/internal/config" +) + +type toolListItem struct { + Name string + Type string + Description string + Shadow *config.ShadowConfig + Hidden bool +} + +type toolGroup struct { + Prefix string + Tools []toolListItem +} + +func groupTools(items []toolListItem) []toolGroup { + groups := make(map[string][]toolListItem) + var order []string + for _, item := range items { + prefix := "" + if i := strings.Index(item.Name, "."); i > 0 { + prefix = item.Name[:i] + } + if _, exists := groups[prefix]; !exists { + order = append(order, prefix) + } + groups[prefix] = append(groups[prefix], item) + } + // Sort: ungrouped first, then alphabetical + sort.Slice(order, func(i, j int) bool { + if order[i] == "" { + return true + } + if order[j] == "" { + return false + } + return order[i] < order[j] + }) + result := make([]toolGroup, len(order)) + for i, prefix := range order { + result[i] = toolGroup{Prefix: prefix, Tools: groups[prefix]} + } + return result +} + +func (s *Server) handleToolsList(w http.ResponseWriter, r *http.Request) { + var tools []toolListItem + for name, tc := range s.cfg.Tools { + if tc.Type == "workflow" { + continue + } + tools = append(tools, toolListItem{ + Name: name, + Type: tc.Type, + Description: tc.Description, + Shadow: tc.Shadow, + Hidden: tc.Hidden, + }) + } + sort.Slice(tools, func(i, j int) bool { return tools[i].Name < tools[j].Name }) + + s.render(w, "tools.html", map[string]any{ + "Title": "Tools", + "Nav": "tools", + "Tools": tools, + }) +} + +func (s *Server) handleToolEdit(w http.ResponseWriter, r *http.Request) { + name := r.PathValue("name") + tc, ok := s.cfg.Tools[name] + if !ok { + http.NotFound(w, r) + return + } + + // Redirect workflows to their dedicated editor + if tc.Type == "workflow" { + http.Redirect(w, r, "/workflows/"+name, http.StatusFound) + return + } + + // Combine tool config name with struct for template + type toolView struct { + Name string + config.ToolConfig + } + + s.render(w, "tool_edit.html", map[string]any{ + "Title": name, + "Nav": "tools", + "ActiveTool": name, + "Tool": toolView{Name: name, ToolConfig: tc}, + "Params": tc.Parameters, + "IsBuiltin": tc.Type == "builtin", + }) +} + +func (s *Server) handleToolTryPanel(w http.ResponseWriter, r *http.Request) { + name := r.PathValue("name") + tc, ok := s.cfg.Tools[name] + if !ok { + http.NotFound(w, r) + return + } + s.renderPartial(w, "try_panel", map[string]any{ + "Name": name, + "Params": tc.Parameters, + }) +} + +func (s *Server) handleToolTry(w http.ResponseWriter, r *http.Request) { + name := r.PathValue("name") + + if err := r.ParseForm(); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // Extract params from form (prefixed with "param_") + params := make(map[string]string) + for key, vals := range r.Form { + if len(key) > 6 && key[:6] == "param_" { + params[key[6:]] = vals[0] + } + } + + start := time.Now() + ctx := context.Background() + result, execErr := s.proxy.ExecuteWithContext(ctx, name, params, "ui") + duration := time.Since(start) + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + + if execErr != nil { + s.renderBlockedResponse(w, name, duration, execErr) + return + } + + output := "" + status := "success" + statusIcon := "✓" + if result != nil { + output = result.Output + if result.IsError() { + status = "error" + statusIcon = "✗" + if result.Error != "" && output == "" { + output = result.Error + } else if result.Error != "" { + output = output + "\n" + result.Error + } + } + } + + s.renderSuccessResponse(w, name, status, statusIcon, duration, output) +} + +func (s *Server) renderBlockedResponse(w http.ResponseWriter, tool string, dur time.Duration, err error) { + s.renderPartial(w, "try_blocked", map[string]any{ + "Tool": tool, + "DurationMs": dur.Milliseconds(), + "Error": err.Error(), + }) +} + +func (s *Server) renderSuccessResponse(w http.ResponseWriter, tool, status, icon string, dur time.Duration, output string) { + statusColor := "green" + textColor := "green-300" + if status == "error" { + statusColor = "amber" + textColor = "amber-300" + } + + isJSON := len(output) > 0 && (output[0] == '{' || output[0] == '[') + formattedOutput := template.HTMLEscapeString(output) + if isJSON { + tree := formatJSONTree(output) + if tree != "" { + formattedOutput = tree + } + } + + tabID := fmt.Sprintf("tab-%d", time.Now().UnixNano()) + + s.renderPartial(w, "try_success", map[string]any{ + "Tool": tool, + "Status": status, + "StatusColor": statusColor, + "Icon": icon, + "DurationMs": dur.Milliseconds(), + "TabID": tabID, + "TextColor": textColor, + "FormattedOutput": template.HTML(formattedOutput), + "RawOutput": output, + "IsJSON": isJSON, + "OutputSize": len(output), + "Timestamp": time.Now().Format("15:04:05"), + }) +} + +// formatJSONHTML returns syntax-highlighted HTML for JSON content. +func formatJSONHTML(s string) string { + var out strings.Builder + inString := false + isKey := true + i := 0 + for i < len(s) { + ch := s[i] + switch { + case ch == '"' && !inString: + // Start of string + inString = true + j := i + 1 + for j < len(s) && s[j] != '"' { + if s[j] == '\\' { + j++ + } + j++ + } + if j < len(s) { + j++ // include closing quote + } + str := template.HTMLEscapeString(s[i:j]) + if isKey { + out.WriteString(`` + str + ``) + } else { + out.WriteString(`` + str + ``) + } + i = j + isKey = false + continue + case ch == ':' && !inString: + out.WriteString(`:`) + isKey = false + i++ + continue + case ch == ',' && !inString: + out.WriteString(`,`) + isKey = true + i++ + continue + case ch == '{' || ch == '}' || ch == '[' || ch == ']': + out.WriteString(`` + string(ch) + ``) + if ch == '{' { + isKey = true + } + i++ + continue + case (ch >= '0' && ch <= '9') || ch == '-': + j := i + for j < len(s) && (s[j] >= '0' && s[j] <= '9' || s[j] == '.' || s[j] == '-' || s[j] == 'e' || s[j] == 'E' || s[j] == '+') { + j++ + } + out.WriteString(`` + s[i:j] + ``) + i = j + continue + case ch == 't' && i+4 <= len(s) && s[i:i+4] == "true": + out.WriteString(`true`) + i += 4 + continue + case ch == 'f' && i+5 <= len(s) && s[i:i+5] == "false": + out.WriteString(`false`) + i += 5 + continue + case ch == 'n' && i+4 <= len(s) && s[i:i+4] == "null": + out.WriteString(`null`) + i += 4 + continue + default: + // HTML-escape any unexpected characters as a safety net. + switch ch { + case '<': + out.WriteString("<") + case '>': + out.WriteString(">") + case '&': + out.WriteString("&") + default: + out.WriteByte(ch) + } + i++ + } + } + return out.String() +} + +// formatJSONTree renders JSON as a collapsible HTML tree using
/. +func formatJSONTree(s string) string { + var data any + if err := json.Unmarshal([]byte(s), &data); err != nil { + return "" + } + var out strings.Builder + renderJSONNode(&out, "", data, true) + return out.String() +} + +func renderJSONNode(out *strings.Builder, key string, value any, last bool) { + comma := "," + if last { + comma = "" + } + keyHTML := "" + if key != "" { + keyHTML = `"` + template.HTMLEscapeString(key) + `": ` + } + + switch v := value.(type) { + case map[string]any: + if len(v) == 0 { + out.WriteString(`
` + keyHTML + `{}` + comma + `
`) + return + } + out.WriteString(`
` + keyHTML + `{ ` + fmt.Sprintf("%d keys", len(v)) + ``) + out.WriteString(`
`) + keys := make([]string, 0, len(v)) + for k := range v { + keys = append(keys, k) + } + sort.Strings(keys) + for i, k := range keys { + renderJSONNode(out, k, v[k], i == len(keys)-1) + } + out.WriteString(`
`) + out.WriteString(`
}` + comma + `
`) + + case []any: + if len(v) == 0 { + out.WriteString(`
` + keyHTML + `[]` + comma + `
`) + return + } + out.WriteString(`
` + keyHTML + `[ ` + fmt.Sprintf("%d items", len(v)) + ``) + out.WriteString(`
`) + for i, item := range v { + renderJSONNode(out, "", item, i == len(v)-1) + } + out.WriteString(`
`) + out.WriteString(`
]` + comma + `
`) + + case string: + out.WriteString(`
` + keyHTML + `"` + template.HTMLEscapeString(v) + `"` + comma + `
`) + + case float64: + out.WriteString(`
` + keyHTML + `` + fmt.Sprintf("%v", v) + `` + comma + `
`) + + case bool: + out.WriteString(`
` + keyHTML + `` + fmt.Sprintf("%t", v) + `` + comma + `
`) + + case nil: + out.WriteString(`
` + keyHTML + `null` + comma + `
`) + } +} + +func (s *Server) handleToolFormPartial(w http.ResponseWriter, r *http.Request) { + toolType := r.URL.Query().Get("type") + partialName := "tool_form_cli" + switch toolType { + case "rest": + partialName = "tool_form_rest" + case "mcp": + partialName = "tool_form_mcp" + } + s.renderPartial(w, partialName, nil) +} + +func (s *Server) handleToolNew(w http.ResponseWriter, r *http.Request) { + s.render(w, "tool_new.html", map[string]any{ + "Title": "New Tool", + "Nav": "tools", + }) +} + +func (s *Server) handleToolCreate(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + name := r.FormValue("name") + if name == "" { + http.Error(w, "name is required", http.StatusBadRequest) + return + } + + tc := config.ToolConfig{ + Type: r.FormValue("type"), + Description: r.FormValue("description"), + } + + switch tc.Type { + case "cli": + tc.Command = r.FormValue("command") + if args := r.FormValue("args"); args != "" { + tc.Args = splitArgs(args) + } + case "rest": + tc.BaseURL = r.FormValue("base_url") + tc.Method = r.FormValue("method") + tc.Path = r.FormValue("path") + case "mcp": + tc.Command = r.FormValue("command") + if args := r.FormValue("args"); args != "" { + tc.Args = splitArgs(args) + } + tc.URL = r.FormValue("url") + } + + // Parse auth (for REST tools) + if authType := r.FormValue("auth_type"); authType != "" { + auth := &config.AuthConfig{Type: authType} + switch authType { + case "bearer": + auth.Token = r.FormValue("auth_token") + case "header": + auth.Header = r.FormValue("auth_header") + auth.Value = r.FormValue("auth_value") + case "oauth": + auth.Provider = r.FormValue("auth_provider") + } + tc.Auth = auth + } + + if err := SaveTool(s.cfgPath, s.toolsDir, name, tc); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Reload config and registry + s.cfg.Tools[name] = tc + s.registerTool(name, tc) + + http.Redirect(w, r, "/tools/"+name, http.StatusFound) +} + +func (s *Server) handleToolSave(w http.ResponseWriter, r *http.Request) { + name := r.PathValue("name") + if err := r.ParseForm(); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + tc, ok := s.cfg.Tools[name] + if !ok { + http.NotFound(w, r) + return + } + + // Built-in tools: only allow shadow/oversight edits + if tc.Type == "builtin" { + deny := splitComma(r.FormValue("shadow_deny")) + confirmOn := r.FormValue("shadow_confirm") == "on" + rateLimit := r.FormValue("shadow_rate_limit") + if len(deny) > 0 || confirmOn || rateLimit != "" { + sc := &config.ShadowConfig{Deny: deny, RateLimit: rateLimit} + if confirmOn { + sc.Confirm = true + } + tc.Shadow = sc + } else { + tc.Shadow = nil + } + s.cfg.Tools[name] = tc + w.Header().Set("Content-Type", "text/html; charset=utf-8") + fmt.Fprint(w, `✓ Saved`) + return + } + + tc.Description = r.FormValue("description") + tc.Hidden = r.FormValue("hidden") == "on" + tc.Stdin = r.FormValue("stdin") + tc.Timeout = r.FormValue("timeout") + tc.Body = r.FormValue("body") + tc.BodyType = r.FormValue("body_type") + + if mo := r.FormValue("max_output"); mo != "" { + if n, err := strconv.Atoi(mo); err == nil { + tc.MaxOutput = n + } + } + if compress := r.FormValue("compress"); compress != "" { + tc.Compress = splitComma(compress) + } else { + tc.Compress = nil + } + + switch tc.Type { + case "cli": + tc.Command = r.FormValue("command") + if args := r.FormValue("args"); args != "" { + tc.Args = splitArgs(args) + } else { + tc.Args = nil + } + case "rest": + tc.BaseURL = r.FormValue("base_url") + tc.Method = r.FormValue("method") + tc.Path = r.FormValue("path") + // Parse headers + headerKeys := r.Form["header_key[]"] + headerVals := r.Form["header_val[]"] + headers := make(map[string]string) + for i, k := range headerKeys { + k = strings.TrimSpace(k) + if k == "" { + continue + } + v := "" + if i < len(headerVals) { + v = headerVals[i] + } + headers[k] = v + } + if len(headers) > 0 { + tc.Headers = headers + } else { + tc.Headers = nil + } + case "mcp": + tc.Command = r.FormValue("command") + if args := r.FormValue("args"); args != "" { + tc.Args = splitArgs(args) + } else { + tc.Args = nil + } + } + + // Parse parameters + var params []config.ParamConfig + for i := 0; ; i++ { + pname := r.FormValue(fmt.Sprintf("param_name_%d", i)) + if pname == "" { + break + } + params = append(params, config.ParamConfig{ + Name: pname, + Type: r.FormValue(fmt.Sprintf("param_type_%d", i)), + In: r.FormValue(fmt.Sprintf("param_in_%d", i)), + Required: r.FormValue(fmt.Sprintf("param_required_%d", i)) == "on", + Default: r.FormValue(fmt.Sprintf("param_default_%d", i)), + Description: r.FormValue(fmt.Sprintf("param_desc_%d", i)), + }) + } + tc.Parameters = params + + // Parse shadow/oversight + deny := splitComma(r.FormValue("shadow_deny")) + confirmOn := r.FormValue("shadow_confirm") == "on" + rateLimit := r.FormValue("shadow_rate_limit") + if len(deny) > 0 || confirmOn || rateLimit != "" { + sc := &config.ShadowConfig{ + Deny: deny, + RateLimit: rateLimit, + } + if confirmOn { + sc.Confirm = true + } + tc.Shadow = sc + } else { + tc.Shadow = nil + } + + // Parse auth + authType := r.FormValue("auth_type") + if authType != "" { + auth := &config.AuthConfig{Type: authType} + switch authType { + case "bearer": + auth.Token = r.FormValue("auth_token") + case "header": + auth.Header = r.FormValue("auth_header") + auth.Value = r.FormValue("auth_value") + case "oauth": + auth.Provider = r.FormValue("auth_provider") + } + tc.Auth = auth + } else { + tc.Auth = nil + } + + // Parse output filter + fc := parseFilterForm(r) + if fc != nil { + tc.Filter = fc + } else { + tc.Filter = nil + } + + if err := SaveTool(s.cfgPath, s.toolsDir, name, tc); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + s.cfg.Tools[name] = tc + s.registerTool(name, tc) + + // Return inline confirmation for htmx + w.Header().Set("Content-Type", "text/html; charset=utf-8") + fmt.Fprint(w, `✓ Saved`) +} + +func (s *Server) handleToolRename(w http.ResponseWriter, r *http.Request) { + name := r.PathValue("name") + if err := r.ParseForm(); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + tc, ok := s.cfg.Tools[name] + if !ok { + http.NotFound(w, r) + return + } + + // Update description + if desc := r.FormValue("description"); desc != "" || r.Form.Has("description") { + tc.Description = r.FormValue("description") + } + + // Handle rename + newName := strings.TrimSpace(r.FormValue("rename")) + if newName == "" { + newName = name + } + + if newName != name { + if err := DeleteTool(s.cfgPath, s.toolsDir, name); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + delete(s.cfg.Tools, name) + s.unregisterTool(name) + } + + if err := SaveTool(s.cfgPath, s.toolsDir, newName, tc); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + s.cfg.Tools[newName] = tc + s.registerTool(newName, tc) + + // Redirect to the (possibly new) tool page + w.Header().Set("HX-Redirect", "/tools/"+newName) + w.WriteHeader(http.StatusOK) +} + +func (s *Server) handleToolDuplicate(w http.ResponseWriter, r *http.Request) { + name := r.PathValue("name") + tc, ok := s.cfg.Tools[name] + if !ok { + http.NotFound(w, r) + return + } + + // Generate a unique copy name + newName := name + ".copy" + for i := 2; ; i++ { + if _, exists := s.cfg.Tools[newName]; !exists { + break + } + newName = fmt.Sprintf("%s.copy%d", name, i) + } + + if err := SaveTool(s.cfgPath, s.toolsDir, newName, tc); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + s.cfg.Tools[newName] = tc + s.registerTool(newName, tc) + + w.Header().Set("HX-Redirect", "/tools/"+newName) + w.WriteHeader(http.StatusOK) +} + +func (s *Server) handleToolDelete(w http.ResponseWriter, r *http.Request) { + name := r.PathValue("name") + + if err := DeleteTool(s.cfgPath, s.toolsDir, name); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + delete(s.cfg.Tools, name) + s.unregisterTool(name) + + // Return empty response for htmx (redirects via HX-Redirect header) + w.Header().Set("HX-Redirect", "/tools") + w.WriteHeader(http.StatusOK) +} + +func (s *Server) render(w http.ResponseWriter, name string, data any) { + t, ok := s.tmpls["templates/"+name] + if !ok { + http.Error(w, fmt.Sprintf("template %q not found", name), http.StatusInternalServerError) + return + } + + if m, ok := data.(map[string]any); ok { + // Ensure ActivePrefix is always set (templates need it) + if _, has := m["ActivePrefix"]; !has { + m["ActivePrefix"] = "" + } + // Inject sidebar tools for tools-related pages + if m["Nav"] == "tools" { + if _, has := m["SidebarToolGroups"]; !has { + m["SidebarToolGroups"] = groupTools(s.getSidebarTools()) + } + if active, ok := m["ActiveTool"].(string); ok { + if i := strings.Index(active, "."); i > 0 { + m["ActivePrefix"] = active[:i] + } + } + } + // Inject sidebar workflows for workflow pages + if m["Nav"] == "workflows" { + if _, has := m["SidebarWorkflowGroups"]; !has { + m["SidebarWorkflowGroups"] = groupTools(s.getSidebarWorkflows()) + } + if active, ok := m["ActiveTool"].(string); ok { + if i := strings.Index(active, "."); i > 0 { + m["ActivePrefix"] = active[:i] + } + } + } + } + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + if err := t.ExecuteTemplate(w, "layout", data); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +// renderPartial renders a named partial template (from templates/partials/). +// Uses any available page template since partials are parsed with all of them. +func (s *Server) renderPartial(w http.ResponseWriter, partialName string, data any) { + // Use the first available template (all include partials) + var t *template.Template + for _, tmpl := range s.tmpls { + t = tmpl + break + } + if t == nil { + http.Error(w, "no templates available", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "text/html; charset=utf-8") + if err := t.ExecuteTemplate(w, partialName, data); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +// getSidebarItems returns sidebar items filtered by type. +// If onlyType is set, only tools of that type are returned. +// If excludeType is set, tools of that type are excluded. +func (s *Server) getSidebarItems(onlyType, excludeType string) []toolListItem { + var items []toolListItem + for name, tc := range s.cfg.Tools { + if onlyType != "" && tc.Type != onlyType { + continue + } + if excludeType != "" && tc.Type == excludeType { + continue + } + items = append(items, toolListItem{ + Name: name, + Type: tc.Type, + Hidden: tc.Hidden, + }) + } + sort.Slice(items, func(i, j int) bool { return items[i].Name < items[j].Name }) + return items +} + +func (s *Server) getSidebarTools() []toolListItem { + return s.getSidebarItems("", "workflow") +} + +func (s *Server) getSidebarWorkflows() []toolListItem { + return s.getSidebarItems("workflow", "") +} diff --git a/src/internal/ui/handlers_vault.go b/src/internal/ui/handlers_vault.go new file mode 100644 index 0000000..c937ac9 --- /dev/null +++ b/src/internal/ui/handlers_vault.go @@ -0,0 +1,158 @@ +// Copyright 2026 Jordan Sherer +// SPDX-License-Identifier: gpl + +package ui + +import ( + "fmt" + "html" + "net/http" +) + +type vaultSection struct { + Label string + Keys []string + Scope string // "project" or "global" +} + +func (s *Server) handleVault(w http.ResponseWriter, r *http.Request) { + var sections []vaultSection + + if s.projectVault != nil { + if keys, err := s.projectVault.List(); err == nil { + sections = append(sections, vaultSection{Label: "Project vault", Keys: keys, Scope: "project"}) + } + } + if s.globalVault != nil { + if keys, err := s.globalVault.List(); err == nil { + sections = append(sections, vaultSection{Label: "Global vault", Keys: keys, Scope: "global"}) + } + } + + // Fallback: if no separate vaults but we have a combined one + if len(sections) == 0 && s.vault != nil { + if keys, err := s.vault.List(); err == nil { + sections = append(sections, vaultSection{Label: "Vault", Keys: keys, Scope: "default"}) + } + } + + s.render(w, "vault.html", map[string]any{ + "Title": "Vault", + "Nav": "vault", + "Sections": sections, + }) +} + +func (s *Server) handleVaultSet(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + key := r.FormValue("key") + value := r.FormValue("value") + scope := r.FormValue("scope") + if key == "" || value == "" { + http.Error(w, "key and value required", http.StatusBadRequest) + return + } + + backend := s.resolveVaultBackend(scope) + if backend == nil { + http.Error(w, "vault not available", http.StatusServiceUnavailable) + return + } + + if err := backend.Set(key, value); err != nil { + http.Error(w, fmt.Sprintf("vault set: %v", err), http.StatusInternalServerError) + return + } + + s.renderVaultKeys(w) +} + +func (s *Server) handleVaultDelete(w http.ResponseWriter, r *http.Request) { + key := r.PathValue("key") + scope := r.URL.Query().Get("scope") + + backend := s.resolveVaultBackend(scope) + if backend == nil { + http.Error(w, "vault not available", http.StatusServiceUnavailable) + return + } + + if err := backend.Delete(key); err != nil { + http.Error(w, fmt.Sprintf("vault delete: %v", err), http.StatusInternalServerError) + return + } + + s.renderVaultKeys(w) +} + +func (s *Server) resolveVaultBackend(scope string) interface { + Set(key, value string) error + Delete(key string) error + List() ([]string, error) +} { + switch scope { + case "project": + if s.projectVault != nil { + return s.projectVault + } + case "global": + if s.globalVault != nil { + return s.globalVault + } + } + return s.vault +} + +func (s *Server) renderVaultKeys(w http.ResponseWriter) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + + var sections []vaultSection + if s.projectVault != nil { + if keys, err := s.projectVault.List(); err == nil { + sections = append(sections, vaultSection{Label: "Project vault", Keys: keys, Scope: "project"}) + } + } + if s.globalVault != nil { + if keys, err := s.globalVault.List(); err == nil { + sections = append(sections, vaultSection{Label: "Global vault", Keys: keys, Scope: "global"}) + } + } + if len(sections) == 0 && s.vault != nil { + if keys, err := s.vault.List(); err == nil { + sections = append(sections, vaultSection{Label: "Vault", Keys: keys, Scope: "default"}) + } + } + + if len(sections) == 0 { + fmt.Fprint(w, `
No secrets stored.
`) + return + } + + for _, sec := range sections { + fmt.Fprintf(w, `
+
%s (%d keys)
`, sec.Label, len(sec.Keys)) + if len(sec.Keys) == 0 { + fmt.Fprint(w, `
empty
`) + } else { + for _, key := range sec.Keys { + esc := html.EscapeString(key) + fmt.Fprintf(w, `
+ %s +
+ •••••••• + +
+
`, esc, esc, sec.Scope, esc) + } + } + fmt.Fprint(w, `
`) + } +} diff --git a/src/internal/ui/handlers_workflows.go b/src/internal/ui/handlers_workflows.go new file mode 100644 index 0000000..0b406d2 --- /dev/null +++ b/src/internal/ui/handlers_workflows.go @@ -0,0 +1,468 @@ +// Copyright 2026 Jordan Sherer +// SPDX-License-Identifier: gpl + +package ui + +import ( + "context" + "encoding/json" + "fmt" + "html/template" + "net/http" + "sort" + "strconv" + "strings" + "time" + + "github.com/factorly-dev/factorly/internal/config" +) + +func (s *Server) handleWorkflowNew(w http.ResponseWriter, r *http.Request) { + s.render(w, "workflow_new.html", map[string]any{ + "Title": "New Workflow", + "Nav": "workflows", + }) +} + +func (s *Server) handleWorkflowCreate(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + name := r.FormValue("name") + if name == "" { + http.Error(w, "name is required", http.StatusBadRequest) + return + } + + tc := config.ToolConfig{ + Type: "workflow", + Description: r.FormValue("description"), + } + + if err := SaveTool(s.cfgPath, s.toolsDir, name, tc); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + s.cfg.Tools[name] = tc + s.registerTool(name, tc) + + http.Redirect(w, r, "/workflows/"+name, http.StatusFound) +} + +func (s *Server) handleWorkflowsList(w http.ResponseWriter, r *http.Request) { + type wfItem struct { + Name string + Description string + StepCount int + } + var workflows []wfItem + for name, tc := range s.cfg.Tools { + if tc.Type == "workflow" { + workflows = append(workflows, wfItem{ + Name: name, + Description: tc.Description, + StepCount: len(tc.Steps), + }) + } + } + sort.Slice(workflows, func(i, j int) bool { + return workflows[i].Name < workflows[j].Name + }) + + s.render(w, "workflows.html", map[string]any{ + "Title": "Workflows", + "Nav": "workflows", + "Workflows": workflows, + }) +} + +func (s *Server) handleWorkflowEdit(w http.ResponseWriter, r *http.Request) { + name := r.PathValue("name") + tc, ok := s.cfg.Tools[name] + if !ok || tc.Type != "workflow" { + http.NotFound(w, r) + return + } + + // List all non-workflow tools for step dropdown + var available []string + for n, t := range s.cfg.Tools { + if t.Type != "workflow" { + available = append(available, n) + } + } + sort.Strings(available) + + s.render(w, "workflow_edit.html", map[string]any{ + "Title": name, + "Nav": "workflows", + "ActiveTool": name, + "Name": name, + "Description": tc.Description, + "Steps": tc.Steps, + "Params": tc.Parameters, + "AvailableTools": available, + "Tool": tc, + }) +} + +func (s *Server) handleWorkflowAddStep(w http.ResponseWriter, r *http.Request) { + name := r.PathValue("name") + tc, ok := s.cfg.Tools[name] + if !ok { + http.NotFound(w, r) + return + } + + // Count existing steps to determine the new index + idx := len(tc.Steps) + // If steps were added client-side, use query param + if qIdx := r.URL.Query().Get("index"); qIdx != "" { + if n, err := strconv.Atoi(qIdx); err == nil { + idx = n + } + } + + var available []string + for n, t := range s.cfg.Tools { + if t.Type != "workflow" { + available = append(available, n) + } + } + sort.Strings(available) + + s.renderPartial(w, "workflow_step", map[string]any{ + "Index": idx, + "WorkflowName": name, + "AvailableTools": available, + }) +} + +func (s *Server) handleWorkflowStepParams(w http.ResponseWriter, r *http.Request) { + toolName := r.URL.Query().Get("tool") + stepIdx := r.URL.Query().Get("index") + if stepIdx == "" { + stepIdx = "0" + } + + tc, ok := s.cfg.Tools[toolName] + if !ok || len(tc.Parameters) == 0 { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + return // empty response clears the params area + } + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + for _, p := range tc.Parameters { + fmt.Fprintf(w, `
+ + + +
`, stepIdx, template.HTMLEscapeString(p.Name), stepIdx, template.HTMLEscapeString(p.Default)) + } +} + +func (s *Server) handleWorkflowSave(w http.ResponseWriter, r *http.Request) { + name := r.PathValue("name") + if err := r.ParseForm(); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + tc, ok := s.cfg.Tools[name] + if !ok { + http.NotFound(w, r) + return + } + + // Rebuild steps from form + var steps []config.StepConfig + for i := 0; ; i++ { + tool := r.FormValue(fmt.Sprintf("step_tool_%d", i)) + if tool == "" { + break + } + store := r.FormValue(fmt.Sprintf("step_store_%d", i)) + ifCond := r.FormValue(fmt.Sprintf("step_if_%d", i)) + reqCond := r.FormValue(fmt.Sprintf("step_require_%d", i)) + + // Parse params (key[] and val[] arrays) + keys := r.Form[fmt.Sprintf("step_param_key_%d[]", i)] + vals := r.Form[fmt.Sprintf("step_param_val_%d[]", i)] + var params map[string]string + if len(keys) > 0 { + params = make(map[string]string) + for j, k := range keys { + if k == "" { + continue + } + v := "" + if j < len(vals) { + v = vals[j] + } + params[k] = v + } + if len(params) == 0 { + params = nil + } + } + + steps = append(steps, config.StepConfig{ + Tool: tool, + Store: store, + If: ifCond, + Require: reqCond, + Params: params, + }) + } + + // Parse workflow input parameters + var wfParams []config.ParamConfig + for i := 0; ; i++ { + pname := r.FormValue(fmt.Sprintf("wf_param_name_%d", i)) + if pname == "" { + break + } + wfParams = append(wfParams, config.ParamConfig{ + Name: pname, + Type: r.FormValue(fmt.Sprintf("wf_param_type_%d", i)), + Required: r.FormValue(fmt.Sprintf("wf_param_required_%d", i)) == "on", + Default: r.FormValue(fmt.Sprintf("wf_param_default_%d", i)), + Description: r.FormValue(fmt.Sprintf("wf_param_desc_%d", i)), + }) + } + + tc.Description = r.FormValue("description") + tc.Parameters = wfParams + tc.Steps = steps + tc.Hidden = r.FormValue("hidden") == "on" + tc.Timeout = r.FormValue("timeout") + + if mo := r.FormValue("max_output"); mo != "" { + if n, err := strconv.Atoi(mo); err == nil { + tc.MaxOutput = n + } + } else { + tc.MaxOutput = 0 + } + if compress := r.FormValue("compress"); compress != "" { + tc.Compress = splitComma(compress) + } else { + tc.Compress = nil + } + + // Parse shadow/oversight + deny := splitComma(r.FormValue("shadow_deny")) + confirmOn := r.FormValue("shadow_confirm") == "on" + rateLimit := r.FormValue("shadow_rate_limit") + if len(deny) > 0 || confirmOn || rateLimit != "" { + sc := &config.ShadowConfig{Deny: deny, RateLimit: rateLimit} + if confirmOn { + sc.Confirm = true + } + tc.Shadow = sc + } else { + tc.Shadow = nil + } + + // Parse output filter + fc := parseFilterForm(r) + if fc != nil { + tc.Filter = fc + } else { + tc.Filter = nil + } + + if err := SaveTool(s.cfgPath, s.toolsDir, name, tc); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + s.cfg.Tools[name] = tc + s.registerTool(name, tc) + + http.Redirect(w, r, "/workflows/"+name, http.StatusFound) +} + +func (s *Server) handleWorkflowRename(w http.ResponseWriter, r *http.Request) { + name := r.PathValue("name") + if err := r.ParseForm(); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + tc, ok := s.cfg.Tools[name] + if !ok { + http.NotFound(w, r) + return + } + + if r.Form.Has("description") { + tc.Description = r.FormValue("description") + } + + newName := strings.TrimSpace(r.FormValue("rename")) + if newName == "" { + newName = name + } + + if newName != name { + if err := DeleteTool(s.cfgPath, s.toolsDir, name); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + delete(s.cfg.Tools, name) + s.unregisterTool(name) + } + + if err := SaveTool(s.cfgPath, s.toolsDir, newName, tc); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + s.cfg.Tools[newName] = tc + s.registerTool(newName, tc) + + w.Header().Set("HX-Redirect", "/workflows/"+newName) + w.WriteHeader(http.StatusOK) +} + +func (s *Server) handleWorkflowDelete(w http.ResponseWriter, r *http.Request) { + name := r.PathValue("name") + + if err := DeleteTool(s.cfgPath, s.toolsDir, name); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + delete(s.cfg.Tools, name) + s.unregisterTool(name) + + w.Header().Set("HX-Redirect", "/workflows") + w.WriteHeader(http.StatusOK) +} + +func (s *Server) handleWorkflowRun(w http.ResponseWriter, r *http.Request) { + name := r.PathValue("name") + if err := r.ParseForm(); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + params := make(map[string]string) + for key, vals := range r.Form { + if len(key) > 6 && key[:6] == "param_" { + params[key[6:]] = vals[0] + } + } + + start := time.Now() + ctx := context.Background() + result, execErr := s.proxy.ExecuteWithContext(ctx, name, params, "ui") + duration := time.Since(start) + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + + if execErr != nil { + fmt.Fprintf(w, `
+
+ Failed + %dms +
+
%s
+
`, duration.Milliseconds(), template.HTMLEscapeString(execErr.Error())) + return + } + + // Parse workflow JSON output for nice rendering + output := "" + if result != nil { + output = result.Output + } + + var wfResult struct { + Status string `json:"status"` + Steps []struct { + Tool string `json:"tool"` + Status string `json:"status"` + DurationMs int `json:"duration_ms"` + Error string `json:"error,omitempty"` + } `json:"steps"` + Result string `json:"result"` + Error string `json:"error,omitempty"` + } + + if err := json.Unmarshal([]byte(output), &wfResult); err != nil { + // Couldn't parse as workflow JSON — show raw + fmt.Fprintf(w, `
%s
`, + template.HTMLEscapeString(output)) + return + } + + // Render structured workflow result as mini-pipeline + statusIcon := "✓" + statusColor := "green" + if wfResult.Status == "failed" { + statusIcon = "✗" + statusColor = "red" + } + + fmt.Fprintf(w, ` +
+
+
+ %s + %s +
+ %dms +
+
`, + statusColor, statusColor, statusColor, statusColor, statusIcon, statusColor, wfResult.Status, duration.Milliseconds()) + + for _, step := range wfResult.Steps { + icon := "✓" + iconBg := "bg-green-100 text-green-600" + if step.Status == "failed" { + icon = "✗" + iconBg = "bg-red-100 text-red-600" + } else if step.Status == "skipped" { + icon = "—" + iconBg = "bg-gray-100 text-gray-400" + } + dur := "" + if step.DurationMs > 0 { + dur = strconv.Itoa(step.DurationMs) + "ms" + } + fmt.Fprintf(w, ` +
+ %s + %s + %s +
`, iconBg, icon, template.HTMLEscapeString(step.Tool), dur) + if step.Error != "" { + fmt.Fprintf(w, ` +
%s
`, template.HTMLEscapeString(step.Error)) + } + } + + fmt.Fprint(w, ` +
`) + + // Result on dark background + if wfResult.Result != "" { + fmt.Fprintf(w, ` +
+
%s
+
`, template.HTMLEscapeString(wfResult.Result)) + } + if wfResult.Error != "" && wfResult.Result == "" { + fmt.Fprintf(w, ` +
+
%s
+
`, template.HTMLEscapeString(wfResult.Error)) + } + + fmt.Fprint(w, ` +
`) +} diff --git a/src/internal/ui/helpers.go b/src/internal/ui/helpers.go new file mode 100644 index 0000000..91b1bea --- /dev/null +++ b/src/internal/ui/helpers.go @@ -0,0 +1,126 @@ +// Copyright 2026 Jordan Sherer +// SPDX-License-Identifier: gpl + +package ui + +import ( + "net/http" + "strconv" + "strings" + + "github.com/factorly-dev/factorly/internal/output" +) + +func dedup(s []string) []string { + seen := make(map[string]bool, len(s)) + out := make([]string, 0, len(s)) + for _, v := range s { + if !seen[v] { + seen[v] = true + out = append(out, v) + } + } + return out +} + +func splitArgs(s string) []string { + var args []string + var current []byte + inQuote := false + quoteChar := byte(0) + + for i := 0; i < len(s); i++ { + c := s[i] + if inQuote { + if c == quoteChar { + inQuote = false + } else { + current = append(current, c) + } + } else if c == '"' || c == '\'' { + inQuote = true + quoteChar = c + } else if c == ' ' { + if len(current) > 0 { + args = append(args, string(current)) + current = current[:0] + } + } else { + current = append(current, c) + } + } + if len(current) > 0 { + args = append(args, string(current)) + } + return args +} + +func splitComma(s string) []string { + if s == "" { + return nil + } + var result []string + for _, part := range strings.Split(s, ",") { + part = strings.TrimSpace(part) + if part != "" { + result = append(result, part) + } + } + return result +} + +func parseFilterForm(r *http.Request) *output.FilterConfig { + jsonPath := strings.TrimSpace(r.FormValue("filter_json_path")) + keepLines := splitComma(r.FormValue("filter_keep_lines")) + stripLines := splitComma(r.FormValue("filter_strip_lines")) + headLines := 0 + tailLines := 0 + maxLines := 0 + if v := r.FormValue("filter_head_lines"); v != "" { + if n, err := strconv.Atoi(v); err == nil { + headLines = n + } + } + if v := r.FormValue("filter_tail_lines"); v != "" { + if n, err := strconv.Atoi(v); err == nil { + tailLines = n + } + } + if v := r.FormValue("filter_max_lines"); v != "" { + if n, err := strconv.Atoi(v); err == nil { + maxLines = n + } + } + + var replaces []output.ReplaceConfig + if raw := strings.TrimSpace(r.FormValue("filter_replace")); raw != "" { + for _, line := range strings.Split(raw, "\n") { + line = strings.TrimSpace(line) + if line == "" { + continue + } + parts := strings.SplitN(line, "→", 2) + if len(parts) == 2 { + replaces = append(replaces, output.ReplaceConfig{ + Pattern: strings.TrimSpace(parts[0]), + Replacement: strings.TrimSpace(parts[1]), + }) + } + } + } + + if jsonPath == "" && len(keepLines) == 0 && len(stripLines) == 0 && + headLines == 0 && tailLines == 0 && maxLines == 0 && len(replaces) == 0 { + return nil + } + + return &output.FilterConfig{ + JSONPath: jsonPath, + HeadLines: headLines, + TailLines: tailLines, + MaxLines: maxLines, + KeepLines: keepLines, + StripLines: stripLines, + Replace: replaces, + } +} diff --git a/src/internal/ui/resolve.go b/src/internal/ui/resolve.go new file mode 100644 index 0000000..5fad7ed --- /dev/null +++ b/src/internal/ui/resolve.go @@ -0,0 +1,54 @@ +// Copyright 2026 Jordan Sherer +// SPDX-License-Identifier: gpl + +package ui + +// resolveRef resolves all backend references (e.g., {{vault:KEY}}, {{op:KEY}}) +// in a string using the shared vault.Resolver. +func (s *Server) resolveRef(val string) string { + if s.resolver == nil || val == "" { + return val + } + resolved, err := s.resolver.Resolve(val) + if err != nil { + return val + } + return resolved +} + +// resolveRefT resolves all backend refs in a value and appends accessed keys to the tracker. +func (s *Server) resolveRefT(val string, keys *[]string) string { + if s.resolver == nil || val == "" { + return val + } + resolved, accessed, err := s.resolver.ResolveTracked(val) + if err != nil { + return val + } + *keys = append(*keys, accessed...) + return resolved +} + +// resolveRefsTracked resolves backend references in a string slice and collects accessed keys. +func (s *Server) resolveRefsTracked(vals []string, keys *[]string) []string { + if s.resolver == nil || len(vals) == 0 { + return vals + } + out := make([]string, len(vals)) + for i, v := range vals { + out[i] = s.resolveRefT(v, keys) + } + return out +} + +// resolveRefMapTracked resolves backend references in a map and collects accessed keys. +func (s *Server) resolveRefMapTracked(m map[string]string, keys *[]string) map[string]string { + if s.resolver == nil || len(m) == 0 { + return m + } + out := make(map[string]string, len(m)) + for k, v := range m { + out[k] = s.resolveRefT(v, keys) + } + return out +} diff --git a/src/internal/ui/server.go b/src/internal/ui/server.go new file mode 100644 index 0000000..86d9d2a --- /dev/null +++ b/src/internal/ui/server.go @@ -0,0 +1,606 @@ +// Copyright 2026 Jordan Sherer +// SPDX-License-Identifier: gpl + +package ui + +import ( + "fmt" + "html/template" + "io/fs" + "net/http" + "os" + "reflect" + "strings" + "time" + + "github.com/factorly-dev/factorly/internal/config" + "github.com/factorly-dev/factorly/internal/oauth" + "github.com/factorly-dev/factorly/internal/output" + "github.com/factorly-dev/factorly/internal/provider" + "github.com/factorly-dev/factorly/internal/proxy" + "github.com/factorly-dev/factorly/internal/registry" + "github.com/factorly-dev/factorly/internal/shadow" + "github.com/factorly-dev/factorly/internal/templates" + "github.com/factorly-dev/factorly/internal/vault" +) + +// Server serves the Factorly web UI. +type Server struct { + cfg *config.Config + cfgPath string + toolsDir string + registry *registry.Registry + proxy *proxy.Proxy + vault vault.Backend + resolver *vault.Resolver + projectVault vault.Backend + globalVault vault.Backend + tmpls map[string]*template.Template + mux *http.ServeMux + mcpHandler http.Handler + activity *ActivityBroadcaster + confirmBroker *ConfirmBroker +} + +// Options configures the UI server. +type Options struct { + Config *config.Config + CfgPath string + ToolsDir string + Registry *registry.Registry + Proxy *proxy.Proxy + Vault vault.Backend + Resolver *vault.Resolver + ProjectVault vault.Backend + GlobalVault vault.Backend + Activity *ActivityBroadcaster + ConfirmBroker *ConfirmBroker +} + +// New creates a UI server. +func New(opts Options) (*Server, error) { + // Parse each page template individually with the shared layout. + // This avoids "content" redefinition conflicts between pages. + pages := []string{ + "templates/tools.html", + "templates/tool_edit.html", + "templates/tool_new.html", + "templates/import.html", + "templates/templates.html", + "templates/template_detail.html", + "templates/workflows.html", + "templates/workflow_new.html", + "templates/workflow_edit.html", + "templates/history.html", + "templates/auth.html", + "templates/vault.html", + } + tmpls := make(map[string]*template.Template, len(pages)) + for _, page := range pages { + t, err := template.New("").Funcs(templateFuncs()).ParseFS(templatesFS, + "templates/layout.html", + "templates/partials/*.html", + page, + ) + if err != nil { + return nil, fmt.Errorf("parsing template %s: %w", page, err) + } + tmpls[page] = t + } + + s := &Server{ + cfg: opts.Config, + cfgPath: opts.CfgPath, + toolsDir: opts.ToolsDir, + registry: opts.Registry, + proxy: opts.Proxy, + vault: opts.Vault, + resolver: opts.Resolver, + projectVault: opts.ProjectVault, + globalVault: opts.GlobalVault, + activity: opts.Activity, + confirmBroker: opts.ConfirmBroker, + tmpls: tmpls, + mux: http.NewServeMux(), + } + + s.routes() + return s, nil +} + +func (s *Server) routes() { + // Static assets (embedded under static/ subdir) + staticSub, _ := fs.Sub(staticFS, "static") + s.mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServerFS(staticSub))) + s.mux.HandleFunc("GET /favicon.ico", func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, "/static/logo.png", http.StatusMovedPermanently) + }) + + // Tools + s.mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, "/tools", http.StatusFound) + }) + s.mux.HandleFunc("GET /tools", s.handleToolsList) + s.mux.HandleFunc("GET /tools/new", s.handleToolNew) + s.mux.HandleFunc("GET /tools/_form", s.handleToolFormPartial) + s.mux.HandleFunc("GET /tools/{name}", s.handleToolEdit) + s.mux.HandleFunc("POST /tools/_new", s.handleToolCreate) + s.mux.HandleFunc("POST /tools/{name}", s.handleToolSave) + s.mux.HandleFunc("POST /tools/{name}/duplicate", s.handleToolDuplicate) + s.mux.HandleFunc("POST /tools/{name}/rename", s.handleToolRename) + s.mux.HandleFunc("GET /tools/{name}/try-panel", s.handleToolTryPanel) + s.mux.HandleFunc("POST /tools/{name}/try", s.handleToolTry) + s.mux.HandleFunc("DELETE /tools/{name}", s.handleToolDelete) + + // Import + s.mux.HandleFunc("GET /tools/import", s.handleImport) + s.mux.HandleFunc("POST /tools/import/preview", s.handleImportPreview) + s.mux.HandleFunc("POST /tools/import/confirm", s.handleImportConfirm) + + // Templates + s.mux.HandleFunc("GET /templates", s.handleTemplatesList) + s.mux.HandleFunc("GET /templates/{name}", s.handleTemplateDetail) + s.mux.HandleFunc("POST /templates/{name}/install", s.handleTemplateInstall) + + // Workflows + s.mux.HandleFunc("GET /workflows", s.handleWorkflowsList) + s.mux.HandleFunc("GET /workflows/new", s.handleWorkflowNew) + s.mux.HandleFunc("POST /workflows/_new", s.handleWorkflowCreate) + s.mux.HandleFunc("GET /workflows/{name}", s.handleWorkflowEdit) + s.mux.HandleFunc("POST /workflows/{name}/rename", s.handleWorkflowRename) + s.mux.HandleFunc("GET /workflows/{name}/add-step", s.handleWorkflowAddStep) + s.mux.HandleFunc("GET /workflows/{name}/step-params", s.handleWorkflowStepParams) + s.mux.HandleFunc("POST /workflows/{name}/save", s.handleWorkflowSave) + s.mux.HandleFunc("POST /workflows/{name}/run", s.handleWorkflowRun) + s.mux.HandleFunc("DELETE /workflows/{name}", s.handleWorkflowDelete) + + // Activity (live feed) + s.mux.HandleFunc("GET /activity", s.handleActivity) + s.mux.HandleFunc("GET /activity/stream", s.handleActivityStream) + + // Confirm (shadow confirm prompts routed to browser) + s.mux.HandleFunc("GET /confirm/pending", s.handleConfirmPending) + s.mux.HandleFunc("GET /confirm/stream", s.handleConfirmSSE) + s.mux.HandleFunc("POST /confirm/{id}/{action}", s.handleConfirmRespond) + + // History + s.mux.HandleFunc("GET /history", s.handleHistory) + + // Auth + s.mux.HandleFunc("GET /auth", s.handleAuth) + s.mux.HandleFunc("POST /auth/_new", s.handleAuthCreate) + s.mux.HandleFunc("POST /auth/{provider}", s.handleAuthUpdate) + s.mux.HandleFunc("POST /auth/{provider}/refresh", s.handleAuthRefresh) + s.mux.HandleFunc("DELETE /auth/{provider}", s.handleAuthDelete) + s.mux.HandleFunc("DELETE /auth/{provider}/token", s.handleAuthLogout) + + // Vault + s.mux.HandleFunc("GET /vault", s.handleVault) + s.mux.HandleFunc("POST /vault", s.handleVaultSet) + s.mux.HandleFunc("DELETE /vault/{key}", s.handleVaultDelete) + + // Reload + s.mux.HandleFunc("POST /reload", s.handleReload) +} + +// MountMCP sets the MCP handler. The StreamableHTTPServer is its own +// root handler (it internally routes /mcp), so we intercept requests +// before the UI mux and delegate /mcp paths to it directly. +func (s *Server) MountMCP(handler http.Handler) { + s.mcpHandler = handler +} + +// Handler returns the HTTP handler for the UI (for wrapping with middleware). +// If MCP is mounted, /mcp requests are delegated to the StreamableHTTPServer +// which acts as its own root handler (it internally routes /mcp). +func (s *Server) Handler() http.Handler { + if s.mcpHandler == nil { + return s.mux + } + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.HasPrefix(r.URL.Path, "/mcp") { + s.mcpHandler.ServeHTTP(w, r) + return + } + s.mux.ServeHTTP(w, r) + }) +} + +// Start begins serving the UI. +func (s *Server) Start(addr string) error { + fmt.Fprintf(os.Stderr, "Factorly UI running at http://%s\n", addr) + return http.ListenAndServe(addr, s.mux) +} + +// registerTool adds or updates a tool in the live registry so it can be +// executed via Try It and MCP without restarting the server. +func (s *Server) registerTool(name string, tc config.ToolConfig) { + if s.registry == nil { + return + } + params := make([]registry.Parameter, len(tc.Parameters)) + for i, p := range tc.Parameters { + params[i] = registry.Parameter{ + Name: p.Name, + Description: p.Description, + Required: p.Required, + Type: p.Type, + Default: p.Default, + Min: p.Min, + Max: p.Max, + MinLength: p.MinLength, + MaxLength: p.MaxLength, + Pattern: p.Pattern, + Enum: p.Enum, + } + } + tool := ®istry.Tool{ + Name: name, + Type: tc.Type, + Description: tc.Description, + Hidden: tc.Hidden, + Parameters: params, + ProviderKey: tc.Type, + MaxOutput: tc.MaxOutput, + Compress: tc.Compress, + Filter: output.CompileFilter(tc.Filter), + } + if tc.Shadow != nil { + var overrides []string + overrides = append(overrides, tc.Shadow.AllowPatterns...) + overrides = append(overrides, tc.Shadow.AllowPaths...) + overrides = append(overrides, tc.Shadow.AllowURLs...) + if len(overrides) > 0 { + tool.AllowOverrides = overrides + } + } + s.registry.Register(tool) + + // Update shadow policy if tool has oversight rules + s.updateShadowRule(name, tc) + + // Register with the appropriate provider and track vault keys for audit + vaultKeys := s.registerProvider(name, tc) + if len(vaultKeys) > 0 { + tool.VaultKeys = dedup(vaultKeys) + } +} + +// registerProvider adds the tool definition to the appropriate provider. +// Returns vault keys that were accessed during resolution. +func (s *Server) registerProvider(name string, tc config.ToolConfig) []string { + if s.proxy == nil { + return nil + } + var vaultKeys []string + switch tc.Type { + case "cli": + s.registerCLIProvider(name, tc, &vaultKeys) + case "rest": + s.registerRESTProvider(name, tc, &vaultKeys) + case "workflow": + s.registerWorkflowProvider(name, tc) + } + return vaultKeys +} + +func (s *Server) registerCLIProvider(name string, tc config.ToolConfig, vaultKeys *[]string) { + prov := s.proxy.Provider("cli") + if prov == nil { + cp := provider.NewCLI(map[string]provider.CLIToolDef{}) + _ = cp.Setup() + s.proxy.RegisterProvider("cli", cp) + prov = cp + } + cp, ok := prov.(*provider.CLIProvider) + if !ok { + return + } + def := provider.CLIToolDef{ + Command: s.resolveRefT(tc.Command, vaultKeys), + Args: s.resolveRefsTracked(tc.Args, vaultKeys), + Stdin: s.resolveRefT(tc.Stdin, vaultKeys), + Interactive: tc.Interactive, + Env: s.resolveRefMapTracked(tc.Env, vaultKeys), + EnvStrict: tc.EnvIsolation == "strict", + } + if tc.Timeout != "" { + if d, err := time.ParseDuration(tc.Timeout); err == nil { + def.Timeout = d + } + } + cp.AddTool(name, def) +} + +func (s *Server) registerRESTProvider(name string, tc config.ToolConfig, vaultKeys *[]string) { + prov := s.proxy.Provider("rest") + if prov == nil { + rp := provider.NewREST(map[string]provider.RESTToolDef{}, nil) + _ = rp.Setup() + s.proxy.RegisterProvider("rest", rp) + prov = rp + } + rp, ok := prov.(*provider.RESTProvider) + if !ok { + return + } + var vaultRefs []string + collectRef := func(v string) { + if vault.HasVaultRefs(v) { + vaultRefs = append(vaultRefs, v) + } + } + collectRef(tc.BaseURL) + collectRef(tc.Path) + if tc.Auth != nil { + collectRef(tc.Auth.Token) + collectRef(tc.Auth.Value) + } + def := provider.RESTToolDef{ + Method: tc.Method, + BaseURL: s.resolveRefT(tc.BaseURL, vaultKeys), + Path: s.resolveRefT(tc.Path, vaultKeys), + Body: tc.Body, + BodyType: tc.BodyType, + Headers: s.resolveRefMapTracked(tc.Headers, vaultKeys), + } + if tc.Auth != nil { + def.Auth = &provider.AuthDef{ + Type: tc.Auth.Type, + Token: s.resolveRefT(tc.Auth.Token, vaultKeys), + Header: tc.Auth.Header, + Value: s.resolveRefT(tc.Auth.Value, vaultKeys), + } + if tc.Auth.Type == "oauth" && s.cfg != nil { + oauthCfg := s.cfg.ResolveOAuthProvider(tc.Auth) + if oauthCfg != nil { + collectRef(oauthCfg.ClientID) + collectRef(oauthCfg.ClientSecret) + def.Auth.OAuthProvider = &oauth.ProviderConfig{ + ClientID: s.resolveRefT(oauthCfg.ClientID, vaultKeys), + ClientSecret: s.resolveRefT(oauthCfg.ClientSecret, vaultKeys), + AuthURL: oauthCfg.AuthURL, + TokenURL: oauthCfg.TokenURL, + Scopes: oauthCfg.Scopes, + } + def.Auth.TokenKey = config.OAuthTokenKey(tc.Auth) + } + } + } + for _, p := range tc.Parameters { + def.Params = append(def.Params, provider.RESTParamDef{ + Name: p.Name, + In: p.In, + Required: p.Required, + Type: p.Type, + }) + } + if tc.Timeout != "" { + if d, err := time.ParseDuration(tc.Timeout); err == nil { + def.Timeout = d + } + } + if len(vaultRefs) > 0 && s.resolver != nil { + refs := vaultRefs // capture for closure + res := s.resolver // capture for closure + def.RedactSecrets = func(str string) string { + return res.Redact(str, refs) + } + } + rp.AddTool(name, def) +} + +func (s *Server) registerWorkflowProvider(name string, tc config.ToolConfig) { + prov := s.proxy.Provider("workflow") + if prov == nil { + return + } + wp, ok := prov.(*provider.WorkflowProvider) + if !ok { + return + } + steps := make([]provider.WorkflowStep, len(tc.Steps)) + for i, st := range tc.Steps { + ws := provider.WorkflowStep{ + Tool: st.Tool, + Params: st.Params, + Store: st.Store, + If: st.If, + Require: st.Require, + } + for _, sc := range st.Switch { + ws.Switch = append(ws.Switch, provider.WorkflowSwitchCase{ + Condition: sc.Condition, + Tool: sc.Tool, + Params: sc.Params, + Store: sc.Store, + }) + } + steps[i] = ws + } + wp.RegisterWorkflow(name, steps) +} + +// unregisterTool removes a tool from the live registry and provider. +func (s *Server) unregisterTool(name string) { + if s.registry == nil { + return + } + // Remove from provider + if s.proxy != nil { + if tc, ok := s.cfg.Tools[name]; ok { + if prov := s.proxy.Provider(tc.Type); prov != nil { + switch p := prov.(type) { + case *provider.CLIProvider: + p.RemoveTool(name) + case *provider.RESTProvider: + p.RemoveTool(name) + case *provider.WorkflowProvider: + p.RemoveWorkflow(name) + } + } + } + } + // Remove shadow rule + if s.proxy != nil { + if policy := s.proxy.Shadow(); policy != nil { + policy.RemoveRule(name) + } + } + s.registry.Unregister(name) +} + +// updateShadowRule syncs shadow/oversight config to the live shadow policy. +func (s *Server) updateShadowRule(name string, tc config.ToolConfig) { + if s.proxy == nil { + return + } + policy := s.proxy.Shadow() + if policy == nil { + return + } + + if tc.Shadow == nil { + policy.RemoveRule(name) + return + } + + confirmList, confirmAll := tc.Shadow.ConfirmList() + rule := &shadow.Rule{ + Deny: tc.Shadow.Deny, + Confirm: confirmList, + ConfirmAll: confirmAll, + LogParams: tc.Shadow.LogParams, + } + if tc.Shadow.RateLimit != "" { + rl, _ := shadow.ParseRateLimit(tc.Shadow.RateLimit) + rule.RateLimit = rl + } + policy.SetRule(name, rule) +} + +// handleReload re-reads config from disk and applies deltas to the live +// registry, providers, and shadow policy without restarting. +func (s *Server) handleReload(w http.ResponseWriter, r *http.Request) { + newCfg, err := config.Load(s.cfgPath) + if err != nil { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + fmt.Fprintf(w, `✗ %s`, template.HTMLEscapeString(err.Error())) + return + } + + var added, removed, updated int + + // Find removed tools (in old but not new) + for name := range s.cfg.Tools { + if _, exists := newCfg.Tools[name]; !exists { + s.unregisterTool(name) + removed++ + } + } + + // Find added and changed tools + for name, newTC := range newCfg.Tools { + oldTC, exists := s.cfg.Tools[name] + if !exists { + added++ + } else if !reflect.DeepEqual(oldTC, newTC) { + s.unregisterTool(name) + updated++ + } else { + continue // unchanged + } + s.registerTool(name, newTC) + } + + // Update oauth providers + s.cfg.Tools = newCfg.Tools + s.cfg.OAuthProviders = newCfg.OAuthProviders + + // Tell browser to refresh the page so sidebar/content updates + w.Header().Set("HX-Refresh", "true") + w.Header().Set("Content-Type", "text/html; charset=utf-8") + fmt.Fprintf(w, `✓ Reloaded (%d added, %d updated, %d removed)`, + added, updated, removed) +} + +func templateFuncs() template.FuncMap { + return template.FuncMap{ + "inc": func(i int) int { return i + 1 }, + "joinList": func(items []string) string { + return strings.Join(items, ", ") + }, + "icon": func(name string) template.HTML { + icons := map[string]string{ + "play": ``, + "send": ``, + "plus": ``, + "trash": ``, + "check": ``, + "x": ``, + "shield": ``, + "terminal": ``, + "globe": ``, + "workflow": ``, + "clock": ``, + "lock": ``, + "package": ``, + } + if svg, ok := icons[name]; ok { + return template.HTML(svg) + } + return "" + }, + "joinArgs": func(args []string) string { + var parts []string + for _, a := range args { + if strings.Contains(a, " ") { + parts = append(parts, `"`+a+`"`) + } else { + parts = append(parts, a) + } + } + return strings.Join(parts, " ") + }, + "shadowSummary": func(sc *config.ShadowConfig) string { + if sc == nil { + return "none" + } + parts := []string{} + if len(sc.Deny) > 0 { + parts = append(parts, fmt.Sprintf("deny:%d", len(sc.Deny))) + } + confirmList, confirmAll := sc.ConfirmList() + if confirmAll { + parts = append(parts, "confirm:all") + } else if len(confirmList) > 0 { + parts = append(parts, fmt.Sprintf("confirm:%d", len(confirmList))) + } + if sc.RateLimit != "" { + parts = append(parts, sc.RateLimit) + } + if len(parts) == 0 { + return "none" + } + result := "" + for i, p := range parts { + if i > 0 { + result += ", " + } + result += p + } + return result + }, + "templateCategories": func() []string { + seen := map[string]bool{} + var cats []string + for _, t := range templates.All() { + if !seen[t.Category] { + seen[t.Category] = true + cats = append(cats, t.Category) + } + } + return cats + }, + } +} diff --git a/src/internal/ui/sse.go b/src/internal/ui/sse.go new file mode 100644 index 0000000..47f691d --- /dev/null +++ b/src/internal/ui/sse.go @@ -0,0 +1,41 @@ +// Copyright 2026 Jordan Sherer +// SPDX-License-Identifier: gpl + +package ui + +import ( + "fmt" + "net/http" + "strings" +) + +// SSEWriter writes Server-Sent Events to an HTTP response. +type SSEWriter struct { + w http.ResponseWriter + flusher http.Flusher +} + +// NewSSEWriter creates an SSE writer, setting appropriate headers. +func NewSSEWriter(w http.ResponseWriter) (*SSEWriter, error) { + flusher, ok := w.(http.Flusher) + if !ok { + return nil, fmt.Errorf("streaming not supported") + } + + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + flusher.Flush() + + return &SSEWriter{w: w, flusher: flusher}, nil +} + +// Event sends a named event with data. +func (sw *SSEWriter) Event(event, data string) { + fmt.Fprintf(sw.w, "event: %s\n", event) + for _, line := range strings.Split(data, "\n") { + fmt.Fprintf(sw.w, "data: %s\n", line) + } + fmt.Fprint(sw.w, "\n") + sw.flusher.Flush() +} diff --git a/src/internal/ui/static/activity.js b/src/internal/ui/static/activity.js new file mode 100644 index 0000000..9c35a20 --- /dev/null +++ b/src/internal/ui/static/activity.js @@ -0,0 +1,111 @@ +// Copyright 2026 Jordan Sherer +// SPDX-License-Identifier: gpl + +// Activity drawer — use var to avoid redeclaration on hx-boost navigation +if (typeof activityOpen === 'undefined') { + var activityOpen = false; + var activityCount = 0; +} + +function toggleActivity() { + activityOpen = !activityOpen; + document.getElementById('activity-drawer').classList.toggle('open', activityOpen); +} + +function clearActivity() { + document.getElementById('activity-feed').innerHTML = '
Waiting for agent activity...
'; + activityCount = 0; + document.getElementById('drawer-count').textContent = '0 calls'; + document.getElementById('activity-badge').textContent = 'Activity'; +} + +// SSE connection (singleton — only create once across hx-boost navigations) +// Use DOMContentLoaded to ensure page is ready; for hx-boost navigations +// the script re-executes after swap so the guard prevents duplicates. +function _initActivitySSE() { + // Reconnect if previous connection was closed (e.g., by auth redirect) + if (window._activitySSE && window._activitySSE.readyState === EventSource.CLOSED) { + window._activitySSE = null; + } + if (window._activitySSE) return; + window._activitySSE = new EventSource('/activity/stream'); + + window._activitySSE.onmessage = function(event) { + const data = JSON.parse(event.data); + const feed = document.getElementById('activity-feed'); + + // Remove empty state + const empty = document.getElementById('activity-empty'); + if (empty) empty.remove(); + + let icon = '✓'; + let iconClass = 'bg-green-100 text-green-600'; + if (data.status === 'blocked') { + icon = '✗'; + iconClass = 'bg-red-100 text-red-600'; + } else if (data.status === 'error') { + icon = '!'; + iconClass = 'bg-amber-100 text-amber-600'; + } + + let shadow = ''; + if (data.shadow && data.shadow !== 'allowed') { + shadow = `${escapeHtml(data.shadow)}`; + } + + let paramsHtml = ''; + if (data.params && Object.keys(data.params).length > 0) { + paramsHtml = '
Params
'; + for (const [k, v] of Object.entries(data.params)) { + paramsHtml += `
${escapeHtml(k)}
${escapeHtml(v)}
`; + } + paramsHtml += '
'; + } + + let detailHtml = ''; + if (data.error) { + detailHtml += `
${escapeHtml(data.error)}
`; + } + if (data.output) { + detailHtml += `
${escapeHtml(data.output)}
`; + } + + const row = document.createElement('details'); + row.className = 'border-b border-gray-100'; + row.innerHTML = ` + +
+ ${icon} + ${escapeHtml(data.tool)} + ${shadow} +
+
+ ${data.duration_ms}ms + ${data.timestamp} +
+
+
+ ${paramsHtml} + ${detailHtml} + ${!paramsHtml && !detailHtml ? 'No details' : ''} +
+ `; + + feed.insertBefore(row, feed.firstChild); + + activityCount++; + document.getElementById('drawer-count').textContent = activityCount + (activityCount === 1 ? ' call' : ' calls'); + document.getElementById('activity-badge').textContent = 'Activity (' + activityCount + ')'; + + // Flash the dot + const dot = document.getElementById('activity-dot'); + dot.classList.remove('bg-green-500'); + dot.classList.add('bg-indigo-500'); + setTimeout(() => { dot.classList.remove('bg-indigo-500'); dot.classList.add('bg-green-500'); }, 300); + }; +} +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', _initActivitySSE); +} else { + _initActivitySSE(); +} diff --git a/src/internal/ui/static/confirm.js b/src/internal/ui/static/confirm.js new file mode 100644 index 0000000..3504fc7 --- /dev/null +++ b/src/internal/ui/static/confirm.js @@ -0,0 +1,104 @@ +// Copyright 2026 Jordan Sherer +// SPDX-License-Identifier: gpl + +// Confirm modal — SSE stream for shadow confirm prompts with polling fallback +(function() { + var sse = null; + var pollTimer = null; + var reconnectTimer = null; + var reconnectDelay = 1000; + + function renderConfirm(pending) { + var modal = document.getElementById('confirm-modal'); + if (!modal) return; + if (!pending || pending.length === 0) { + modal.classList.add('hidden'); + return; + } + var req = pending[0]; + var paramsHtml = ''; + if (req.params && Object.keys(req.params).length > 0) { + paramsHtml = '
'; + for (var k in req.params) { + paramsHtml += '
' + escapeHtml(k) + '' + escapeHtml(req.params[k]) + '
'; + } + paramsHtml += '
'; + } + document.getElementById('confirm-content').innerHTML = + '
' + + '!' + + '' + escapeHtml(req.tool) + '' + + '
' + + '

This tool requires confirmation before executing.

' + + paramsHtml + + '
' + + '' + + '' + + '
'; + modal.classList.remove('hidden'); + } + + function pollPending() { + fetch('/confirm/pending') + .then(function(r) { return r.ok ? r.json() : []; }) + .then(renderConfirm) + .catch(function() {}); + } + + function startPolling() { + if (pollTimer) return; + // Always poll as a safety net — catches prompts even if SSE is broken + pollTimer = setInterval(pollPending, 2000); + } + + function connectSSE() { + if (sse && sse.readyState !== EventSource.CLOSED) return; + + sse = new EventSource('/confirm/stream'); + reconnectDelay = 1000; + + sse.onmessage = function(event) { + try { + var pending = JSON.parse(event.data); + renderConfirm(pending); + } catch(e) {} + }; + + sse.onerror = function() { + if (sse.readyState === EventSource.CLOSED) { + sse = null; + scheduleReconnect(); + } + // If CONNECTING, EventSource is auto-reconnecting — do nothing + }; + } + + function scheduleReconnect() { + if (reconnectTimer) return; + reconnectTimer = setTimeout(function() { + reconnectTimer = null; + connectSSE(); + }, reconnectDelay); + reconnectDelay = Math.min(reconnectDelay * 2, 15000); + } + + function init() { + connectSSE(); + // Immediate check for anything pending right now + pollPending(); + // Always keep polling as a fallback — SSE provides instant updates, + // polling catches anything SSE misses (disconnects, races, etc.) + startPolling(); + } + + window.respondConfirm = function(id, action) { + fetch('/confirm/' + id + '/' + action, { method: 'POST' }); + document.getElementById('confirm-modal').classList.add('hidden'); + }; + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } +})(); diff --git a/src/internal/ui/static/htmx.min.js b/src/internal/ui/static/htmx.min.js new file mode 100644 index 0000000..59937d7 --- /dev/null +++ b/src/internal/ui/static/htmx.min.js @@ -0,0 +1 @@ +var htmx=function(){"use strict";const Q={onLoad:null,process:null,on:null,off:null,trigger:null,ajax:null,find:null,findAll:null,closest:null,values:function(e,t){const n=cn(e,t||"post");return n.values},remove:null,addClass:null,removeClass:null,toggleClass:null,takeClass:null,swap:null,defineExtension:null,removeExtension:null,logAll:null,logNone:null,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,allowScriptTags:true,inlineScriptNonce:"",inlineStyleNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",scrollBehavior:"instant",defaultFocusScroll:false,getCacheBusterParam:false,globalViewTransitions:false,methodsThatUseUrlParams:["get","delete"],selfRequestsOnly:true,ignoreTitle:false,scrollIntoViewOnBoost:true,triggerSpecsCache:null,disableInheritance:false,responseHandling:[{code:"204",swap:false},{code:"[23]..",swap:true},{code:"[45]..",swap:false,error:true}],allowNestedOobSwaps:true},parseInterval:null,_:null,version:"2.0.4"};Q.onLoad=j;Q.process=kt;Q.on=ye;Q.off=be;Q.trigger=he;Q.ajax=Rn;Q.find=u;Q.findAll=x;Q.closest=g;Q.remove=z;Q.addClass=K;Q.removeClass=G;Q.toggleClass=W;Q.takeClass=Z;Q.swap=$e;Q.defineExtension=Fn;Q.removeExtension=Bn;Q.logAll=V;Q.logNone=_;Q.parseInterval=d;Q._=e;const n={addTriggerHandler:St,bodyContains:le,canAccessLocalStorage:B,findThisElement:Se,filterValues:hn,swap:$e,hasAttribute:s,getAttributeValue:te,getClosestAttributeValue:re,getClosestMatch:o,getExpressionVars:En,getHeaders:fn,getInputValues:cn,getInternalData:ie,getSwapSpecification:gn,getTriggerSpecs:st,getTarget:Ee,makeFragment:P,mergeObjects:ce,makeSettleInfo:xn,oobSwap:He,querySelectorExt:ae,settleImmediately:Kt,shouldCancel:ht,triggerEvent:he,triggerErrorEvent:fe,withExtensions:Ft};const r=["get","post","put","delete","patch"];const H=r.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");function d(e){if(e==undefined){return undefined}let t=NaN;if(e.slice(-2)=="ms"){t=parseFloat(e.slice(0,-2))}else if(e.slice(-1)=="s"){t=parseFloat(e.slice(0,-1))*1e3}else if(e.slice(-1)=="m"){t=parseFloat(e.slice(0,-1))*1e3*60}else{t=parseFloat(e)}return isNaN(t)?undefined:t}function ee(e,t){return e instanceof Element&&e.getAttribute(t)}function s(e,t){return!!e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function te(e,t){return ee(e,t)||ee(e,"data-"+t)}function c(e){const t=e.parentElement;if(!t&&e.parentNode instanceof ShadowRoot)return e.parentNode;return t}function ne(){return document}function m(e,t){return e.getRootNode?e.getRootNode({composed:t}):ne()}function o(e,t){while(e&&!t(e)){e=c(e)}return e||null}function i(e,t,n){const r=te(t,n);const o=te(t,"hx-disinherit");var i=te(t,"hx-inherit");if(e!==t){if(Q.config.disableInheritance){if(i&&(i==="*"||i.split(" ").indexOf(n)>=0)){return r}else{return null}}if(o&&(o==="*"||o.split(" ").indexOf(n)>=0)){return"unset"}}return r}function re(t,n){let r=null;o(t,function(e){return!!(r=i(t,ue(e),n))});if(r!=="unset"){return r}}function h(e,t){const n=e instanceof Element&&(e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector);return!!n&&n.call(e,t)}function T(e){const t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;const n=t.exec(e);if(n){return n[1].toLowerCase()}else{return""}}function q(e){const t=new DOMParser;return t.parseFromString(e,"text/html")}function L(e,t){while(t.childNodes.length>0){e.append(t.childNodes[0])}}function A(e){const t=ne().createElement("script");se(e.attributes,function(e){t.setAttribute(e.name,e.value)});t.textContent=e.textContent;t.async=false;if(Q.config.inlineScriptNonce){t.nonce=Q.config.inlineScriptNonce}return t}function N(e){return e.matches("script")&&(e.type==="text/javascript"||e.type==="module"||e.type==="")}function I(e){Array.from(e.querySelectorAll("script")).forEach(e=>{if(N(e)){const t=A(e);const n=e.parentNode;try{n.insertBefore(t,e)}catch(e){O(e)}finally{e.remove()}}})}function P(e){const t=e.replace(/]*)?>[\s\S]*?<\/head>/i,"");const n=T(t);let r;if(n==="html"){r=new DocumentFragment;const i=q(e);L(r,i.body);r.title=i.title}else if(n==="body"){r=new DocumentFragment;const i=q(t);L(r,i.body);r.title=i.title}else{const i=q('");r=i.querySelector("template").content;r.title=i.title;var o=r.querySelector("title");if(o&&o.parentNode===r){o.remove();r.title=o.innerText}}if(r){if(Q.config.allowScriptTags){I(r)}else{r.querySelectorAll("script").forEach(e=>e.remove())}}return r}function oe(e){if(e){e()}}function t(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function k(e){return typeof e==="function"}function D(e){return t(e,"Object")}function ie(e){const t="htmx-internal-data";let n=e[t];if(!n){n=e[t]={}}return n}function M(t){const n=[];if(t){for(let e=0;e=0}function le(e){return e.getRootNode({composed:true})===document}function F(e){return e.trim().split(/\s+/)}function ce(e,t){for(const n in t){if(t.hasOwnProperty(n)){e[n]=t[n]}}return e}function S(e){try{return JSON.parse(e)}catch(e){O(e);return null}}function B(){const e="htmx:localStorageTest";try{localStorage.setItem(e,e);localStorage.removeItem(e);return true}catch(e){return false}}function U(t){try{const e=new URL(t);if(e){t=e.pathname+e.search}if(!/^\/$/.test(t)){t=t.replace(/\/+$/,"")}return t}catch(e){return t}}function e(e){return vn(ne().body,function(){return eval(e)})}function j(t){const e=Q.on("htmx:load",function(e){t(e.detail.elt)});return e}function V(){Q.logger=function(e,t,n){if(console){console.log(t,e,n)}}}function _(){Q.logger=null}function u(e,t){if(typeof e!=="string"){return e.querySelector(t)}else{return u(ne(),e)}}function x(e,t){if(typeof e!=="string"){return e.querySelectorAll(t)}else{return x(ne(),e)}}function E(){return window}function z(e,t){e=y(e);if(t){E().setTimeout(function(){z(e);e=null},t)}else{c(e).removeChild(e)}}function ue(e){return e instanceof Element?e:null}function $(e){return e instanceof HTMLElement?e:null}function J(e){return typeof e==="string"?e:null}function f(e){return e instanceof Element||e instanceof Document||e instanceof DocumentFragment?e:null}function K(e,t,n){e=ue(y(e));if(!e){return}if(n){E().setTimeout(function(){K(e,t);e=null},n)}else{e.classList&&e.classList.add(t)}}function G(e,t,n){let r=ue(y(e));if(!r){return}if(n){E().setTimeout(function(){G(r,t);r=null},n)}else{if(r.classList){r.classList.remove(t);if(r.classList.length===0){r.removeAttribute("class")}}}}function W(e,t){e=y(e);e.classList.toggle(t)}function Z(e,t){e=y(e);se(e.parentElement.children,function(e){G(e,t)});K(ue(e),t)}function g(e,t){e=ue(y(e));if(e&&e.closest){return e.closest(t)}else{do{if(e==null||h(e,t)){return e}}while(e=e&&ue(c(e)));return null}}function l(e,t){return e.substring(0,t.length)===t}function Y(e,t){return e.substring(e.length-t.length)===t}function ge(e){const t=e.trim();if(l(t,"<")&&Y(t,"/>")){return t.substring(1,t.length-2)}else{return t}}function p(t,r,n){if(r.indexOf("global ")===0){return p(t,r.slice(7),true)}t=y(t);const o=[];{let t=0;let n=0;for(let e=0;e"){t--}}if(n0){const r=ge(o.shift());let e;if(r.indexOf("closest ")===0){e=g(ue(t),ge(r.substr(8)))}else if(r.indexOf("find ")===0){e=u(f(t),ge(r.substr(5)))}else if(r==="next"||r==="nextElementSibling"){e=ue(t).nextElementSibling}else if(r.indexOf("next ")===0){e=pe(t,ge(r.substr(5)),!!n)}else if(r==="previous"||r==="previousElementSibling"){e=ue(t).previousElementSibling}else if(r.indexOf("previous ")===0){e=me(t,ge(r.substr(9)),!!n)}else if(r==="document"){e=document}else if(r==="window"){e=window}else if(r==="body"){e=document.body}else if(r==="root"){e=m(t,!!n)}else if(r==="host"){e=t.getRootNode().host}else{s.push(r)}if(e){i.push(e)}}if(s.length>0){const e=s.join(",");const c=f(m(t,!!n));i.push(...M(c.querySelectorAll(e)))}return i}var pe=function(t,e,n){const r=f(m(t,n)).querySelectorAll(e);for(let e=0;e=0;e--){const o=r[e];if(o.compareDocumentPosition(t)===Node.DOCUMENT_POSITION_FOLLOWING){return o}}};function ae(e,t){if(typeof e!=="string"){return p(e,t)[0]}else{return p(ne().body,e)[0]}}function y(e,t){if(typeof e==="string"){return u(f(t)||document,e)}else{return e}}function xe(e,t,n,r){if(k(t)){return{target:ne().body,event:J(e),listener:t,options:n}}else{return{target:y(e),event:J(t),listener:n,options:r}}}function ye(t,n,r,o){Vn(function(){const e=xe(t,n,r,o);e.target.addEventListener(e.event,e.listener,e.options)});const e=k(n);return e?n:r}function be(t,n,r){Vn(function(){const e=xe(t,n,r);e.target.removeEventListener(e.event,e.listener)});return k(n)?n:r}const ve=ne().createElement("output");function we(e,t){const n=re(e,t);if(n){if(n==="this"){return[Se(e,t)]}else{const r=p(e,n);if(r.length===0){O('The selector "'+n+'" on '+t+" returned no matches!");return[ve]}else{return r}}}}function Se(e,t){return ue(o(e,function(e){return te(ue(e),t)!=null}))}function Ee(e){const t=re(e,"hx-target");if(t){if(t==="this"){return Se(e,"hx-target")}else{return ae(e,t)}}else{const n=ie(e);if(n.boosted){return ne().body}else{return e}}}function Ce(t){const n=Q.config.attributesToSettle;for(let e=0;e0){s=e.substring(0,e.indexOf(":"));n=e.substring(e.indexOf(":")+1)}else{s=e}o.removeAttribute("hx-swap-oob");o.removeAttribute("data-hx-swap-oob");const r=p(t,n,false);if(r){se(r,function(e){let t;const n=o.cloneNode(true);t=ne().createDocumentFragment();t.appendChild(n);if(!Re(s,e)){t=f(n)}const r={shouldSwap:true,target:e,fragment:t};if(!he(e,"htmx:oobBeforeSwap",r))return;e=r.target;if(r.shouldSwap){qe(t);_e(s,e,e,t,i);Te()}se(i.elts,function(e){he(e,"htmx:oobAfterSwap",r)})});o.parentNode.removeChild(o)}else{o.parentNode.removeChild(o);fe(ne().body,"htmx:oobErrorNoTarget",{content:o})}return e}function Te(){const e=u("#--htmx-preserve-pantry--");if(e){for(const t of[...e.children]){const n=u("#"+t.id);n.parentNode.moveBefore(t,n);n.remove()}e.remove()}}function qe(e){se(x(e,"[hx-preserve], [data-hx-preserve]"),function(e){const t=te(e,"id");const n=ne().getElementById(t);if(n!=null){if(e.moveBefore){let e=u("#--htmx-preserve-pantry--");if(e==null){ne().body.insertAdjacentHTML("afterend","
");e=u("#--htmx-preserve-pantry--")}e.moveBefore(n,null)}else{e.parentNode.replaceChild(n,e)}}})}function Le(l,e,c){se(e.querySelectorAll("[id]"),function(t){const n=ee(t,"id");if(n&&n.length>0){const r=n.replace("'","\\'");const o=t.tagName.replace(":","\\:");const e=f(l);const i=e&&e.querySelector(o+"[id='"+r+"']");if(i&&i!==e){const s=t.cloneNode();Oe(t,i);c.tasks.push(function(){Oe(t,s)})}}})}function Ae(e){return function(){G(e,Q.config.addedClass);kt(ue(e));Ne(f(e));he(e,"htmx:load")}}function Ne(e){const t="[autofocus]";const n=$(h(e,t)?e:e.querySelector(t));if(n!=null){n.focus()}}function a(e,t,n,r){Le(e,n,r);while(n.childNodes.length>0){const o=n.firstChild;K(ue(o),Q.config.addedClass);e.insertBefore(o,t);if(o.nodeType!==Node.TEXT_NODE&&o.nodeType!==Node.COMMENT_NODE){r.tasks.push(Ae(o))}}}function Ie(e,t){let n=0;while(n0}function $e(e,t,r,o){if(!o){o={}}e=y(e);const i=o.contextElement?m(o.contextElement,false):ne();const n=document.activeElement;let s={};try{s={elt:n,start:n?n.selectionStart:null,end:n?n.selectionEnd:null}}catch(e){}const l=xn(e);if(r.swapStyle==="textContent"){e.textContent=t}else{let n=P(t);l.title=n.title;if(o.selectOOB){const u=o.selectOOB.split(",");for(let t=0;t0){E().setTimeout(c,r.settleDelay)}else{c()}}function Je(e,t,n){const r=e.getResponseHeader(t);if(r.indexOf("{")===0){const o=S(r);for(const i in o){if(o.hasOwnProperty(i)){let e=o[i];if(D(e)){n=e.target!==undefined?e.target:n}else{e={value:e}}he(n,i,e)}}}else{const s=r.split(",");for(let e=0;e0){const s=o[0];if(s==="]"){e--;if(e===0){if(n===null){t=t+"true"}o.shift();t+=")})";try{const l=vn(r,function(){return Function(t)()},function(){return true});l.source=t;return l}catch(e){fe(ne().body,"htmx:syntax:error",{error:e,source:t});return null}}}else if(s==="["){e++}if(tt(s,n,i)){t+="(("+i+"."+s+") ? ("+i+"."+s+") : (window."+s+"))"}else{t=t+s}n=o.shift()}}}function C(e,t){let n="";while(e.length>0&&!t.test(e[0])){n+=e.shift()}return n}function rt(e){let t;if(e.length>0&&Ye.test(e[0])){e.shift();t=C(e,Qe).trim();e.shift()}else{t=C(e,v)}return t}const ot="input, textarea, select";function it(e,t,n){const r=[];const o=et(t);do{C(o,w);const l=o.length;const c=C(o,/[,\[\s]/);if(c!==""){if(c==="every"){const u={trigger:"every"};C(o,w);u.pollInterval=d(C(o,/[,\[\s]/));C(o,w);var i=nt(e,o,"event");if(i){u.eventFilter=i}r.push(u)}else{const a={trigger:c};var i=nt(e,o,"event");if(i){a.eventFilter=i}C(o,w);while(o.length>0&&o[0]!==","){const f=o.shift();if(f==="changed"){a.changed=true}else if(f==="once"){a.once=true}else if(f==="consume"){a.consume=true}else if(f==="delay"&&o[0]===":"){o.shift();a.delay=d(C(o,v))}else if(f==="from"&&o[0]===":"){o.shift();if(Ye.test(o[0])){var s=rt(o)}else{var s=C(o,v);if(s==="closest"||s==="find"||s==="next"||s==="previous"){o.shift();const h=rt(o);if(h.length>0){s+=" "+h}}}a.from=s}else if(f==="target"&&o[0]===":"){o.shift();a.target=rt(o)}else if(f==="throttle"&&o[0]===":"){o.shift();a.throttle=d(C(o,v))}else if(f==="queue"&&o[0]===":"){o.shift();a.queue=C(o,v)}else if(f==="root"&&o[0]===":"){o.shift();a[f]=rt(o)}else if(f==="threshold"&&o[0]===":"){o.shift();a[f]=C(o,v)}else{fe(e,"htmx:syntax:error",{token:o.shift()})}C(o,w)}r.push(a)}}if(o.length===l){fe(e,"htmx:syntax:error",{token:o.shift()})}C(o,w)}while(o[0]===","&&o.shift());if(n){n[t]=r}return r}function st(e){const t=te(e,"hx-trigger");let n=[];if(t){const r=Q.config.triggerSpecsCache;n=r&&r[t]||it(e,t,r)}if(n.length>0){return n}else if(h(e,"form")){return[{trigger:"submit"}]}else if(h(e,'input[type="button"], input[type="submit"]')){return[{trigger:"click"}]}else if(h(e,ot)){return[{trigger:"change"}]}else{return[{trigger:"click"}]}}function lt(e){ie(e).cancelled=true}function ct(e,t,n){const r=ie(e);r.timeout=E().setTimeout(function(){if(le(e)&&r.cancelled!==true){if(!gt(n,e,Mt("hx:poll:trigger",{triggerSpec:n,target:e}))){t(e)}ct(e,t,n)}},n.pollInterval)}function ut(e){return location.hostname===e.hostname&&ee(e,"href")&&ee(e,"href").indexOf("#")!==0}function at(e){return g(e,Q.config.disableSelector)}function ft(t,n,e){if(t instanceof HTMLAnchorElement&&ut(t)&&(t.target===""||t.target==="_self")||t.tagName==="FORM"&&String(ee(t,"method")).toLowerCase()!=="dialog"){n.boosted=true;let r,o;if(t.tagName==="A"){r="get";o=ee(t,"href")}else{const i=ee(t,"method");r=i?i.toLowerCase():"get";o=ee(t,"action");if(o==null||o===""){o=ne().location.href}if(r==="get"&&o.includes("?")){o=o.replace(/\?[^#]+/,"")}}e.forEach(function(e){pt(t,function(e,t){const n=ue(e);if(at(n)){b(n);return}de(r,o,n,t)},n,e,true)})}}function ht(e,t){const n=ue(t);if(!n){return false}if(e.type==="submit"||e.type==="click"){if(n.tagName==="FORM"){return true}if(h(n,'input[type="submit"], button')&&(h(n,"[form]")||g(n,"form")!==null)){return true}if(n instanceof HTMLAnchorElement&&n.href&&(n.getAttribute("href")==="#"||n.getAttribute("href").indexOf("#")!==0)){return true}}return false}function dt(e,t){return ie(e).boosted&&e instanceof HTMLAnchorElement&&t.type==="click"&&(t.ctrlKey||t.metaKey)}function gt(e,t,n){const r=e.eventFilter;if(r){try{return r.call(t,n)!==true}catch(e){const o=r.source;fe(ne().body,"htmx:eventFilter:error",{error:e,source:o});return true}}return false}function pt(l,c,e,u,a){const f=ie(l);let t;if(u.from){t=p(l,u.from)}else{t=[l]}if(u.changed){if(!("lastValue"in f)){f.lastValue=new WeakMap}t.forEach(function(e){if(!f.lastValue.has(u)){f.lastValue.set(u,new WeakMap)}f.lastValue.get(u).set(e,e.value)})}se(t,function(i){const s=function(e){if(!le(l)){i.removeEventListener(u.trigger,s);return}if(dt(l,e)){return}if(a||ht(e,l)){e.preventDefault()}if(gt(u,l,e)){return}const t=ie(e);t.triggerSpec=u;if(t.handledFor==null){t.handledFor=[]}if(t.handledFor.indexOf(l)<0){t.handledFor.push(l);if(u.consume){e.stopPropagation()}if(u.target&&e.target){if(!h(ue(e.target),u.target)){return}}if(u.once){if(f.triggeredOnce){return}else{f.triggeredOnce=true}}if(u.changed){const n=event.target;const r=n.value;const o=f.lastValue.get(u);if(o.has(n)&&o.get(n)===r){return}o.set(n,r)}if(f.delayed){clearTimeout(f.delayed)}if(f.throttle){return}if(u.throttle>0){if(!f.throttle){he(l,"htmx:trigger");c(l,e);f.throttle=E().setTimeout(function(){f.throttle=null},u.throttle)}}else if(u.delay>0){f.delayed=E().setTimeout(function(){he(l,"htmx:trigger");c(l,e)},u.delay)}else{he(l,"htmx:trigger");c(l,e)}}};if(e.listenerInfos==null){e.listenerInfos=[]}e.listenerInfos.push({trigger:u.trigger,listener:s,on:i});i.addEventListener(u.trigger,s)})}let mt=false;let xt=null;function yt(){if(!xt){xt=function(){mt=true};window.addEventListener("scroll",xt);window.addEventListener("resize",xt);setInterval(function(){if(mt){mt=false;se(ne().querySelectorAll("[hx-trigger*='revealed'],[data-hx-trigger*='revealed']"),function(e){bt(e)})}},200)}}function bt(e){if(!s(e,"data-hx-revealed")&&X(e)){e.setAttribute("data-hx-revealed","true");const t=ie(e);if(t.initHash){he(e,"revealed")}else{e.addEventListener("htmx:afterProcessNode",function(){he(e,"revealed")},{once:true})}}}function vt(e,t,n,r){const o=function(){if(!n.loaded){n.loaded=true;he(e,"htmx:trigger");t(e)}};if(r>0){E().setTimeout(o,r)}else{o()}}function wt(t,n,e){let i=false;se(r,function(r){if(s(t,"hx-"+r)){const o=te(t,"hx-"+r);i=true;n.path=o;n.verb=r;e.forEach(function(e){St(t,e,n,function(e,t){const n=ue(e);if(g(n,Q.config.disableSelector)){b(n);return}de(r,o,n,t)})})}});return i}function St(r,e,t,n){if(e.trigger==="revealed"){yt();pt(r,n,t,e);bt(ue(r))}else if(e.trigger==="intersect"){const o={};if(e.root){o.root=ae(r,e.root)}if(e.threshold){o.threshold=parseFloat(e.threshold)}const i=new IntersectionObserver(function(t){for(let e=0;e0){t.polling=true;ct(ue(r),n,e)}else{pt(r,n,t,e)}}function Et(e){const t=ue(e);if(!t){return false}const n=t.attributes;for(let e=0;e", "+e).join(""));return o}else{return[]}}function Tt(e){const t=g(ue(e.target),"button, input[type='submit']");const n=Lt(e);if(n){n.lastButtonClicked=t}}function qt(e){const t=Lt(e);if(t){t.lastButtonClicked=null}}function Lt(e){const t=g(ue(e.target),"button, input[type='submit']");if(!t){return}const n=y("#"+ee(t,"form"),t.getRootNode())||g(t,"form");if(!n){return}return ie(n)}function At(e){e.addEventListener("click",Tt);e.addEventListener("focusin",Tt);e.addEventListener("focusout",qt)}function Nt(t,e,n){const r=ie(t);if(!Array.isArray(r.onHandlers)){r.onHandlers=[]}let o;const i=function(e){vn(t,function(){if(at(t)){return}if(!o){o=new Function("event",n)}o.call(t,e)})};t.addEventListener(e,i);r.onHandlers.push({event:e,listener:i})}function It(t){ke(t);for(let e=0;eQ.config.historyCacheSize){i.shift()}while(i.length>0){try{localStorage.setItem("htmx-history-cache",JSON.stringify(i));break}catch(e){fe(ne().body,"htmx:historyCacheError",{cause:e,cache:i});i.shift()}}}function Vt(t){if(!B()){return null}t=U(t);const n=S(localStorage.getItem("htmx-history-cache"))||[];for(let e=0;e=200&&this.status<400){he(ne().body,"htmx:historyCacheMissLoad",i);const e=P(this.response);const t=e.querySelector("[hx-history-elt],[data-hx-history-elt]")||e;const n=Ut();const r=xn(n);kn(e.title);qe(e);Ve(n,t,r);Te();Kt(r.tasks);Bt=o;he(ne().body,"htmx:historyRestore",{path:o,cacheMiss:true,serverResponse:this.response})}else{fe(ne().body,"htmx:historyCacheMissLoadError",i)}};e.send()}function Wt(e){zt();e=e||location.pathname+location.search;const t=Vt(e);if(t){const n=P(t.content);const r=Ut();const o=xn(r);kn(t.title);qe(n);Ve(r,n,o);Te();Kt(o.tasks);E().setTimeout(function(){window.scrollTo(0,t.scroll)},0);Bt=e;he(ne().body,"htmx:historyRestore",{path:e,item:t})}else{if(Q.config.refreshOnHistoryMiss){window.location.reload(true)}else{Gt(e)}}}function Zt(e){let t=we(e,"hx-indicator");if(t==null){t=[e]}se(t,function(e){const t=ie(e);t.requestCount=(t.requestCount||0)+1;e.classList.add.call(e.classList,Q.config.requestClass)});return t}function Yt(e){let t=we(e,"hx-disabled-elt");if(t==null){t=[]}se(t,function(e){const t=ie(e);t.requestCount=(t.requestCount||0)+1;e.setAttribute("disabled","");e.setAttribute("data-disabled-by-htmx","")});return t}function Qt(e,t){se(e.concat(t),function(e){const t=ie(e);t.requestCount=(t.requestCount||1)-1});se(e,function(e){const t=ie(e);if(t.requestCount===0){e.classList.remove.call(e.classList,Q.config.requestClass)}});se(t,function(e){const t=ie(e);if(t.requestCount===0){e.removeAttribute("disabled");e.removeAttribute("data-disabled-by-htmx")}})}function en(t,n){for(let e=0;en.indexOf(e)<0)}else{e=e.filter(e=>e!==n)}r.delete(t);se(e,e=>r.append(t,e))}}function on(t,n,r,o,i){if(o==null||en(t,o)){return}else{t.push(o)}if(tn(o)){const s=ee(o,"name");let e=o.value;if(o instanceof HTMLSelectElement&&o.multiple){e=M(o.querySelectorAll("option:checked")).map(function(e){return e.value})}if(o instanceof HTMLInputElement&&o.files){e=M(o.files)}nn(s,e,n);if(i){sn(o,r)}}if(o instanceof HTMLFormElement){se(o.elements,function(e){if(t.indexOf(e)>=0){rn(e.name,e.value,n)}else{t.push(e)}if(i){sn(e,r)}});new FormData(o).forEach(function(e,t){if(e instanceof File&&e.name===""){return}nn(t,e,n)})}}function sn(e,t){const n=e;if(n.willValidate){he(n,"htmx:validation:validate");if(!n.checkValidity()){t.push({elt:n,message:n.validationMessage,validity:n.validity});he(n,"htmx:validation:failed",{message:n.validationMessage,validity:n.validity})}}}function ln(n,e){for(const t of e.keys()){n.delete(t)}e.forEach(function(e,t){n.append(t,e)});return n}function cn(e,t){const n=[];const r=new FormData;const o=new FormData;const i=[];const s=ie(e);if(s.lastButtonClicked&&!le(s.lastButtonClicked)){s.lastButtonClicked=null}let l=e instanceof HTMLFormElement&&e.noValidate!==true||te(e,"hx-validate")==="true";if(s.lastButtonClicked){l=l&&s.lastButtonClicked.formNoValidate!==true}if(t!=="get"){on(n,o,i,g(e,"form"),l)}on(n,r,i,e,l);if(s.lastButtonClicked||e.tagName==="BUTTON"||e.tagName==="INPUT"&&ee(e,"type")==="submit"){const u=s.lastButtonClicked||e;const a=ee(u,"name");nn(a,u.value,o)}const c=we(e,"hx-include");se(c,function(e){on(n,r,i,ue(e),l);if(!h(e,"form")){se(f(e).querySelectorAll(ot),function(e){on(n,r,i,e,l)})}});ln(r,o);return{errors:i,formData:r,values:An(r)}}function un(e,t,n){if(e!==""){e+="&"}if(String(n)==="[object Object]"){n=JSON.stringify(n)}const r=encodeURIComponent(n);e+=encodeURIComponent(t)+"="+r;return e}function an(e){e=qn(e);let n="";e.forEach(function(e,t){n=un(n,t,e)});return n}function fn(e,t,n){const r={"HX-Request":"true","HX-Trigger":ee(e,"id"),"HX-Trigger-Name":ee(e,"name"),"HX-Target":te(t,"id"),"HX-Current-URL":ne().location.href};bn(e,"hx-headers",false,r);if(n!==undefined){r["HX-Prompt"]=n}if(ie(e).boosted){r["HX-Boosted"]="true"}return r}function hn(n,e){const t=re(e,"hx-params");if(t){if(t==="none"){return new FormData}else if(t==="*"){return n}else if(t.indexOf("not ")===0){se(t.slice(4).split(","),function(e){e=e.trim();n.delete(e)});return n}else{const r=new FormData;se(t.split(","),function(t){t=t.trim();if(n.has(t)){n.getAll(t).forEach(function(e){r.append(t,e)})}});return r}}else{return n}}function dn(e){return!!ee(e,"href")&&ee(e,"href").indexOf("#")>=0}function gn(e,t){const n=t||re(e,"hx-swap");const r={swapStyle:ie(e).boosted?"innerHTML":Q.config.defaultSwapStyle,swapDelay:Q.config.defaultSwapDelay,settleDelay:Q.config.defaultSettleDelay};if(Q.config.scrollIntoViewOnBoost&&ie(e).boosted&&!dn(e)){r.show="top"}if(n){const s=F(n);if(s.length>0){for(let e=0;e0?o.join(":"):null;r.scroll=u;r.scrollTarget=i}else if(l.indexOf("show:")===0){const a=l.slice(5);var o=a.split(":");const f=o.pop();var i=o.length>0?o.join(":"):null;r.show=f;r.showTarget=i}else if(l.indexOf("focus-scroll:")===0){const h=l.slice("focus-scroll:".length);r.focusScroll=h=="true"}else if(e==0){r.swapStyle=l}else{O("Unknown modifier in hx-swap: "+l)}}}}return r}function pn(e){return re(e,"hx-encoding")==="multipart/form-data"||h(e,"form")&&ee(e,"enctype")==="multipart/form-data"}function mn(t,n,r){let o=null;Ft(n,function(e){if(o==null){o=e.encodeParameters(t,r,n)}});if(o!=null){return o}else{if(pn(n)){return ln(new FormData,qn(r))}else{return an(r)}}}function xn(e){return{tasks:[],elts:[e]}}function yn(e,t){const n=e[0];const r=e[e.length-1];if(t.scroll){var o=null;if(t.scrollTarget){o=ue(ae(n,t.scrollTarget))}if(t.scroll==="top"&&(n||o)){o=o||n;o.scrollTop=0}if(t.scroll==="bottom"&&(r||o)){o=o||r;o.scrollTop=o.scrollHeight}}if(t.show){var o=null;if(t.showTarget){let e=t.showTarget;if(t.showTarget==="window"){e="body"}o=ue(ae(n,e))}if(t.show==="top"&&(n||o)){o=o||n;o.scrollIntoView({block:"start",behavior:Q.config.scrollBehavior})}if(t.show==="bottom"&&(r||o)){o=o||r;o.scrollIntoView({block:"end",behavior:Q.config.scrollBehavior})}}}function bn(r,e,o,i){if(i==null){i={}}if(r==null){return i}const s=te(r,e);if(s){let e=s.trim();let t=o;if(e==="unset"){return null}if(e.indexOf("javascript:")===0){e=e.slice(11);t=true}else if(e.indexOf("js:")===0){e=e.slice(3);t=true}if(e.indexOf("{")!==0){e="{"+e+"}"}let n;if(t){n=vn(r,function(){return Function("return ("+e+")")()},{})}else{n=S(e)}for(const l in n){if(n.hasOwnProperty(l)){if(i[l]==null){i[l]=n[l]}}}}return bn(ue(c(r)),e,o,i)}function vn(e,t,n){if(Q.config.allowEval){return t()}else{fe(e,"htmx:evalDisallowedError");return n}}function wn(e,t){return bn(e,"hx-vars",true,t)}function Sn(e,t){return bn(e,"hx-vals",false,t)}function En(e){return ce(wn(e),Sn(e))}function Cn(t,n,r){if(r!==null){try{t.setRequestHeader(n,r)}catch(e){t.setRequestHeader(n,encodeURIComponent(r));t.setRequestHeader(n+"-URI-AutoEncoded","true")}}}function On(t){if(t.responseURL&&typeof URL!=="undefined"){try{const e=new URL(t.responseURL);return e.pathname+e.search}catch(e){fe(ne().body,"htmx:badResponseUrl",{url:t.responseURL})}}}function R(e,t){return t.test(e.getAllResponseHeaders())}function Rn(t,n,r){t=t.toLowerCase();if(r){if(r instanceof Element||typeof r==="string"){return de(t,n,null,null,{targetOverride:y(r)||ve,returnPromise:true})}else{let e=y(r.target);if(r.target&&!e||r.source&&!e&&!y(r.source)){e=ve}return de(t,n,y(r.source),r.event,{handler:r.handler,headers:r.headers,values:r.values,targetOverride:e,swapOverride:r.swap,select:r.select,returnPromise:true})}}else{return de(t,n,null,null,{returnPromise:true})}}function Hn(e){const t=[];while(e){t.push(e);e=e.parentElement}return t}function Tn(e,t,n){let r;let o;if(typeof URL==="function"){o=new URL(t,document.location.href);const i=document.location.origin;r=i===o.origin}else{o=t;r=l(t,document.location.origin)}if(Q.config.selfRequestsOnly){if(!r){return false}}return he(e,"htmx:validateUrl",ce({url:o,sameHost:r},n))}function qn(e){if(e instanceof FormData)return e;const t=new FormData;for(const n in e){if(e.hasOwnProperty(n)){if(e[n]&&typeof e[n].forEach==="function"){e[n].forEach(function(e){t.append(n,e)})}else if(typeof e[n]==="object"&&!(e[n]instanceof Blob)){t.append(n,JSON.stringify(e[n]))}else{t.append(n,e[n])}}}return t}function Ln(r,o,e){return new Proxy(e,{get:function(t,e){if(typeof e==="number")return t[e];if(e==="length")return t.length;if(e==="push"){return function(e){t.push(e);r.append(o,e)}}if(typeof t[e]==="function"){return function(){t[e].apply(t,arguments);r.delete(o);t.forEach(function(e){r.append(o,e)})}}if(t[e]&&t[e].length===1){return t[e][0]}else{return t[e]}},set:function(e,t,n){e[t]=n;r.delete(o);e.forEach(function(e){r.append(o,e)});return true}})}function An(o){return new Proxy(o,{get:function(e,t){if(typeof t==="symbol"){const r=Reflect.get(e,t);if(typeof r==="function"){return function(){return r.apply(o,arguments)}}else{return r}}if(t==="toJSON"){return()=>Object.fromEntries(o)}if(t in e){if(typeof e[t]==="function"){return function(){return o[t].apply(o,arguments)}}else{return e[t]}}const n=o.getAll(t);if(n.length===0){return undefined}else if(n.length===1){return n[0]}else{return Ln(e,t,n)}},set:function(t,n,e){if(typeof n!=="string"){return false}t.delete(n);if(e&&typeof e.forEach==="function"){e.forEach(function(e){t.append(n,e)})}else if(typeof e==="object"&&!(e instanceof Blob)){t.append(n,JSON.stringify(e))}else{t.append(n,e)}return true},deleteProperty:function(e,t){if(typeof t==="string"){e.delete(t)}return true},ownKeys:function(e){return Reflect.ownKeys(Object.fromEntries(e))},getOwnPropertyDescriptor:function(e,t){return Reflect.getOwnPropertyDescriptor(Object.fromEntries(e),t)}})}function de(t,n,r,o,i,D){let s=null;let l=null;i=i!=null?i:{};if(i.returnPromise&&typeof Promise!=="undefined"){var e=new Promise(function(e,t){s=e;l=t})}if(r==null){r=ne().body}const M=i.handler||Dn;const X=i.select||null;if(!le(r)){oe(s);return e}const c=i.targetOverride||ue(Ee(r));if(c==null||c==ve){fe(r,"htmx:targetError",{target:te(r,"hx-target")});oe(l);return e}let u=ie(r);const a=u.lastButtonClicked;if(a){const L=ee(a,"formaction");if(L!=null){n=L}const A=ee(a,"formmethod");if(A!=null){if(A.toLowerCase()!=="dialog"){t=A}}}const f=re(r,"hx-confirm");if(D===undefined){const K=function(e){return de(t,n,r,o,i,!!e)};const G={target:c,elt:r,path:n,verb:t,triggeringEvent:o,etc:i,issueRequest:K,question:f};if(he(r,"htmx:confirm",G)===false){oe(s);return e}}let h=r;let d=re(r,"hx-sync");let g=null;let F=false;if(d){const N=d.split(":");const I=N[0].trim();if(I==="this"){h=Se(r,"hx-sync")}else{h=ue(ae(r,I))}d=(N[1]||"drop").trim();u=ie(h);if(d==="drop"&&u.xhr&&u.abortable!==true){oe(s);return e}else if(d==="abort"){if(u.xhr){oe(s);return e}else{F=true}}else if(d==="replace"){he(h,"htmx:abort")}else if(d.indexOf("queue")===0){const W=d.split(" ");g=(W[1]||"last").trim()}}if(u.xhr){if(u.abortable){he(h,"htmx:abort")}else{if(g==null){if(o){const P=ie(o);if(P&&P.triggerSpec&&P.triggerSpec.queue){g=P.triggerSpec.queue}}if(g==null){g="last"}}if(u.queuedRequests==null){u.queuedRequests=[]}if(g==="first"&&u.queuedRequests.length===0){u.queuedRequests.push(function(){de(t,n,r,o,i)})}else if(g==="all"){u.queuedRequests.push(function(){de(t,n,r,o,i)})}else if(g==="last"){u.queuedRequests=[];u.queuedRequests.push(function(){de(t,n,r,o,i)})}oe(s);return e}}const p=new XMLHttpRequest;u.xhr=p;u.abortable=F;const m=function(){u.xhr=null;u.abortable=false;if(u.queuedRequests!=null&&u.queuedRequests.length>0){const e=u.queuedRequests.shift();e()}};const B=re(r,"hx-prompt");if(B){var x=prompt(B);if(x===null||!he(r,"htmx:prompt",{prompt:x,target:c})){oe(s);m();return e}}if(f&&!D){if(!confirm(f)){oe(s);m();return e}}let y=fn(r,c,x);if(t!=="get"&&!pn(r)){y["Content-Type"]="application/x-www-form-urlencoded"}if(i.headers){y=ce(y,i.headers)}const U=cn(r,t);let b=U.errors;const j=U.formData;if(i.values){ln(j,qn(i.values))}const V=qn(En(r));const v=ln(j,V);let w=hn(v,r);if(Q.config.getCacheBusterParam&&t==="get"){w.set("org.htmx.cache-buster",ee(c,"id")||"true")}if(n==null||n===""){n=ne().location.href}const S=bn(r,"hx-request");const _=ie(r).boosted;let E=Q.config.methodsThatUseUrlParams.indexOf(t)>=0;const C={boosted:_,useUrlParams:E,formData:w,parameters:An(w),unfilteredFormData:v,unfilteredParameters:An(v),headers:y,target:c,verb:t,errors:b,withCredentials:i.credentials||S.credentials||Q.config.withCredentials,timeout:i.timeout||S.timeout||Q.config.timeout,path:n,triggeringEvent:o};if(!he(r,"htmx:configRequest",C)){oe(s);m();return e}n=C.path;t=C.verb;y=C.headers;w=qn(C.parameters);b=C.errors;E=C.useUrlParams;if(b&&b.length>0){he(r,"htmx:validation:halted",C);oe(s);m();return e}const z=n.split("#");const $=z[0];const O=z[1];let R=n;if(E){R=$;const Z=!w.keys().next().done;if(Z){if(R.indexOf("?")<0){R+="?"}else{R+="&"}R+=an(w);if(O){R+="#"+O}}}if(!Tn(r,R,C)){fe(r,"htmx:invalidPath",C);oe(l);return e}p.open(t.toUpperCase(),R,true);p.overrideMimeType("text/html");p.withCredentials=C.withCredentials;p.timeout=C.timeout;if(S.noHeaders){}else{for(const k in y){if(y.hasOwnProperty(k)){const Y=y[k];Cn(p,k,Y)}}}const H={xhr:p,target:c,requestConfig:C,etc:i,boosted:_,select:X,pathInfo:{requestPath:n,finalRequestPath:R,responsePath:null,anchor:O}};p.onload=function(){try{const t=Hn(r);H.pathInfo.responsePath=On(p);M(r,H);if(H.keepIndicators!==true){Qt(T,q)}he(r,"htmx:afterRequest",H);he(r,"htmx:afterOnLoad",H);if(!le(r)){let e=null;while(t.length>0&&e==null){const n=t.shift();if(le(n)){e=n}}if(e){he(e,"htmx:afterRequest",H);he(e,"htmx:afterOnLoad",H)}}oe(s);m()}catch(e){fe(r,"htmx:onLoadError",ce({error:e},H));throw e}};p.onerror=function(){Qt(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:sendError",H);oe(l);m()};p.onabort=function(){Qt(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:sendAbort",H);oe(l);m()};p.ontimeout=function(){Qt(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:timeout",H);oe(l);m()};if(!he(r,"htmx:beforeRequest",H)){oe(s);m();return e}var T=Zt(r);var q=Yt(r);se(["loadstart","loadend","progress","abort"],function(t){se([p,p.upload],function(e){e.addEventListener(t,function(e){he(r,"htmx:xhr:"+t,{lengthComputable:e.lengthComputable,loaded:e.loaded,total:e.total})})})});he(r,"htmx:beforeSend",H);const J=E?null:mn(p,r,w);p.send(J);return e}function Nn(e,t){const n=t.xhr;let r=null;let o=null;if(R(n,/HX-Push:/i)){r=n.getResponseHeader("HX-Push");o="push"}else if(R(n,/HX-Push-Url:/i)){r=n.getResponseHeader("HX-Push-Url");o="push"}else if(R(n,/HX-Replace-Url:/i)){r=n.getResponseHeader("HX-Replace-Url");o="replace"}if(r){if(r==="false"){return{}}else{return{type:o,path:r}}}const i=t.pathInfo.finalRequestPath;const s=t.pathInfo.responsePath;const l=re(e,"hx-push-url");const c=re(e,"hx-replace-url");const u=ie(e).boosted;let a=null;let f=null;if(l){a="push";f=l}else if(c){a="replace";f=c}else if(u){a="push";f=s||i}if(f){if(f==="false"){return{}}if(f==="true"){f=s||i}if(t.pathInfo.anchor&&f.indexOf("#")===-1){f=f+"#"+t.pathInfo.anchor}return{type:a,path:f}}else{return{}}}function In(e,t){var n=new RegExp(e.code);return n.test(t.toString(10))}function Pn(e){for(var t=0;t0){E().setTimeout(e,x.swapDelay)}else{e()}}if(f){fe(o,"htmx:responseError",ce({error:"Response Status Error Code "+s.status+" from "+i.pathInfo.requestPath},i))}}const Mn={};function Xn(){return{init:function(e){return null},getSelectors:function(){return null},onEvent:function(e,t){return true},transformResponse:function(e,t,n){return e},isInlineSwap:function(e){return false},handleSwap:function(e,t,n,r){return false},encodeParameters:function(e,t,n){return null}}}function Fn(e,t){if(t.init){t.init(n)}Mn[e]=ce(Xn(),t)}function Bn(e){delete Mn[e]}function Un(e,n,r){if(n==undefined){n=[]}if(e==undefined){return n}if(r==undefined){r=[]}const t=te(e,"hx-ext");if(t){se(t.split(","),function(e){e=e.replace(/ /g,"");if(e.slice(0,7)=="ignore:"){r.push(e.slice(7));return}if(r.indexOf(e)<0){const t=Mn[e];if(t&&n.indexOf(t)<0){n.push(t)}}})}return Un(ue(c(e)),n,r)}var jn=false;ne().addEventListener("DOMContentLoaded",function(){jn=true});function Vn(e){if(jn||ne().readyState==="complete"){e()}else{ne().addEventListener("DOMContentLoaded",e)}}function _n(){if(Q.config.includeIndicatorStyles!==false){const e=Q.config.inlineStyleNonce?` nonce="${Q.config.inlineStyleNonce}"`:"";ne().head.insertAdjacentHTML("beforeend"," ."+Q.config.indicatorClass+"{opacity:0} ."+Q.config.requestClass+" ."+Q.config.indicatorClass+"{opacity:1; transition: opacity 200ms ease-in;} ."+Q.config.requestClass+"."+Q.config.indicatorClass+"{opacity:1; transition: opacity 200ms ease-in;} ")}}function zn(){const e=ne().querySelector('meta[name="htmx-config"]');if(e){return S(e.content)}else{return null}}function $n(){const e=zn();if(e){Q.config=ce(Q.config,e)}}Vn(function(){$n();_n();let e=ne().body;kt(e);const t=ne().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");e.addEventListener("htmx:abort",function(e){const t=e.target;const n=ie(t);if(n&&n.xhr){n.xhr.abort()}});const n=window.onpopstate?window.onpopstate.bind(window):null;window.onpopstate=function(e){if(e.state&&e.state.htmx){Wt();se(t,function(e){he(e,"htmx:restored",{document:ne(),triggerEvent:he})})}else{if(n){n(e)}}};E().setTimeout(function(){he(e,"htmx:load",{});e=null},0)});return Q}(); \ No newline at end of file diff --git a/src/internal/ui/static/logo.png b/src/internal/ui/static/logo.png new file mode 100644 index 0000000..a99a6d1 Binary files /dev/null and b/src/internal/ui/static/logo.png differ diff --git a/src/internal/ui/static/sse.js b/src/internal/ui/static/sse.js new file mode 100644 index 0000000..9f5af5c --- /dev/null +++ b/src/internal/ui/static/sse.js @@ -0,0 +1,290 @@ +/* +Server Sent Events Extension +============================ +This extension adds support for Server Sent Events to htmx. See /www/extensions/sse.md for usage instructions. + +*/ + +(function() { + /** @type {import("../htmx").HtmxInternalApi} */ + var api + + htmx.defineExtension('sse', { + + /** + * Init saves the provided reference to the internal HTMX API. + * + * @param {import("../htmx").HtmxInternalApi} api + * @returns void + */ + init: function(apiRef) { + // store a reference to the internal API. + api = apiRef + + // set a function in the public API for creating new EventSource objects + if (htmx.createEventSource == undefined) { + htmx.createEventSource = createEventSource + } + }, + + getSelectors: function() { + return ['[sse-connect]', '[data-sse-connect]', '[sse-swap]', '[data-sse-swap]'] + }, + + /** + * onEvent handles all events passed to this extension. + * + * @param {string} name + * @param {Event} evt + * @returns void + */ + onEvent: function(name, evt) { + var parent = evt.target || evt.detail.elt + switch (name) { + case 'htmx:beforeCleanupElement': + var internalData = api.getInternalData(parent) + // Try to remove remove an EventSource when elements are removed + var source = internalData.sseEventSource + if (source) { + api.triggerEvent(parent, 'htmx:sseClose', { + source, + type: 'nodeReplaced', + }) + internalData.sseEventSource.close() + } + + return + + // Try to create EventSources when elements are processed + case 'htmx:afterProcessNode': + ensureEventSourceOnElement(parent) + } + } + }) + + /// //////////////////////////////////////////// + // HELPER FUNCTIONS + /// //////////////////////////////////////////// + + /** + * createEventSource is the default method for creating new EventSource objects. + * it is hoisted into htmx.config.createEventSource to be overridden by the user, if needed. + * + * @param {string} url + * @returns EventSource + */ + function createEventSource(url) { + return new EventSource(url, { withCredentials: true }) + } + + /** + * registerSSE looks for attributes that can contain sse events, right + * now hx-trigger and sse-swap and adds listeners based on these attributes too + * the closest event source + * + * @param {HTMLElement} elt + */ + function registerSSE(elt) { + // Add message handlers for every `sse-swap` attribute + if (api.getAttributeValue(elt, 'sse-swap')) { + // Find closest existing event source + var sourceElement = api.getClosestMatch(elt, hasEventSource) + if (sourceElement == null) { + // api.triggerErrorEvent(elt, "htmx:noSSESourceError") + return null // no eventsource in parentage, orphaned element + } + + // Set internalData and source + var internalData = api.getInternalData(sourceElement) + var source = internalData.sseEventSource + + var sseSwapAttr = api.getAttributeValue(elt, 'sse-swap') + var sseEventNames = sseSwapAttr.split(',') + + for (var i = 0; i < sseEventNames.length; i++) { + const sseEventName = sseEventNames[i].trim() + const listener = function(event) { + // If the source is missing then close SSE + if (maybeCloseSSESource(sourceElement)) { + return + } + + // If the body no longer contains the element, remove the listener + if (!api.bodyContains(elt)) { + source.removeEventListener(sseEventName, listener) + return + } + + // swap the response into the DOM and trigger a notification + if (!api.triggerEvent(elt, 'htmx:sseBeforeMessage', event)) { + return + } + swap(elt, event.data) + api.triggerEvent(elt, 'htmx:sseMessage', event) + } + + // Register the new listener + api.getInternalData(elt).sseEventListener = listener + source.addEventListener(sseEventName, listener) + } + } + + // Add message handlers for every `hx-trigger="sse:*"` attribute + if (api.getAttributeValue(elt, 'hx-trigger')) { + // Find closest existing event source + var sourceElement = api.getClosestMatch(elt, hasEventSource) + if (sourceElement == null) { + // api.triggerErrorEvent(elt, "htmx:noSSESourceError") + return null // no eventsource in parentage, orphaned element + } + + // Set internalData and source + var internalData = api.getInternalData(sourceElement) + var source = internalData.sseEventSource + + var triggerSpecs = api.getTriggerSpecs(elt) + triggerSpecs.forEach(function(ts) { + if (ts.trigger.slice(0, 4) !== 'sse:') { + return + } + + var listener = function (event) { + if (maybeCloseSSESource(sourceElement)) { + return + } + if (!api.bodyContains(elt)) { + source.removeEventListener(ts.trigger.slice(4), listener) + } + // Trigger events to be handled by the rest of htmx + htmx.trigger(elt, ts.trigger, event) + htmx.trigger(elt, 'htmx:sseMessage', event) + } + + // Register the new listener + api.getInternalData(elt).sseEventListener = listener + source.addEventListener(ts.trigger.slice(4), listener) + }) + } + } + + /** + * ensureEventSourceOnElement creates a new EventSource connection on the provided element. + * If a usable EventSource already exists, then it is returned. If not, then a new EventSource + * is created and stored in the element's internalData. + * @param {HTMLElement} elt + * @param {number} retryCount + * @returns {EventSource | null} + */ + function ensureEventSourceOnElement(elt, retryCount) { + if (elt == null) { + return null + } + + // handle extension source creation attribute + if (api.getAttributeValue(elt, 'sse-connect')) { + var sseURL = api.getAttributeValue(elt, 'sse-connect') + if (sseURL == null) { + return + } + + ensureEventSource(elt, sseURL, retryCount) + } + + registerSSE(elt) + } + + function ensureEventSource(elt, url, retryCount) { + var source = htmx.createEventSource(url) + + source.onerror = function(err) { + // Log an error event + api.triggerErrorEvent(elt, 'htmx:sseError', { error: err, source }) + + // If parent no longer exists in the document, then clean up this EventSource + if (maybeCloseSSESource(elt)) { + return + } + + // Otherwise, try to reconnect the EventSource + if (source.readyState === EventSource.CLOSED) { + retryCount = retryCount || 0 + retryCount = Math.max(Math.min(retryCount * 2, 128), 1) + var timeout = retryCount * 500 + window.setTimeout(function() { + ensureEventSourceOnElement(elt, retryCount) + }, timeout) + } + } + + source.onopen = function(evt) { + api.triggerEvent(elt, 'htmx:sseOpen', { source }) + + if (retryCount && retryCount > 0) { + const childrenToFix = elt.querySelectorAll("[sse-swap], [data-sse-swap], [hx-trigger], [data-hx-trigger]") + for (let i = 0; i < childrenToFix.length; i++) { + registerSSE(childrenToFix[i]) + } + // We want to increase the reconnection delay for consecutive failed attempts only + retryCount = 0 + } + } + + api.getInternalData(elt).sseEventSource = source + + + var closeAttribute = api.getAttributeValue(elt, "sse-close"); + if (closeAttribute) { + // close eventsource when this message is received + source.addEventListener(closeAttribute, function() { + api.triggerEvent(elt, 'htmx:sseClose', { + source, + type: 'message', + }) + source.close() + }); + } + } + + /** + * maybeCloseSSESource confirms that the parent element still exists. + * If not, then any associated SSE source is closed and the function returns true. + * + * @param {HTMLElement} elt + * @returns boolean + */ + function maybeCloseSSESource(elt) { + if (!api.bodyContains(elt)) { + var source = api.getInternalData(elt).sseEventSource + if (source != undefined) { + api.triggerEvent(elt, 'htmx:sseClose', { + source, + type: 'nodeMissing', + }) + source.close() + // source = null + return true + } + } + return false + } + + + /** + * @param {HTMLElement} elt + * @param {string} content + */ + function swap(elt, content) { + api.withExtensions(elt, function(extension) { + content = extension.transformResponse(content, null, elt) + }) + + var swapSpec = api.getSwapSpecification(elt) + var target = api.getTarget(elt) + api.swap(target, content, swapSpec) + } + + + function hasEventSource(node) { + return api.getInternalData(node).sseEventSource != null + } +})() diff --git a/src/internal/ui/static/style.css b/src/internal/ui/static/style.css new file mode 100644 index 0000000..39f19a8 --- /dev/null +++ b/src/internal/ui/static/style.css @@ -0,0 +1,2 @@ +/* Factorly UI — minimal custom styles beyond Tailwind */ +.font-mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; } diff --git a/src/internal/ui/static/tailwind.js b/src/internal/ui/static/tailwind.js new file mode 100644 index 0000000..573c165 --- /dev/null +++ b/src/internal/ui/static/tailwind.js @@ -0,0 +1,83 @@ +(()=>{var qv=Object.create;var Hi=Object.defineProperty;var $v=Object.getOwnPropertyDescriptor;var Lv=Object.getOwnPropertyNames;var Mv=Object.getPrototypeOf,Nv=Object.prototype.hasOwnProperty;var df=r=>Hi(r,"__esModule",{value:!0});var hf=r=>{if(typeof require!="undefined")return require(r);throw new Error('Dynamic require of "'+r+'" is not supported')};var P=(r,e)=>()=>(r&&(e=r(r=0)),e);var x=(r,e)=>()=>(e||r((e={exports:{}}).exports,e),e.exports),Ge=(r,e)=>{df(r);for(var t in e)Hi(r,t,{get:e[t],enumerable:!0})},Bv=(r,e,t)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of Lv(e))!Nv.call(r,i)&&i!=="default"&&Hi(r,i,{get:()=>e[i],enumerable:!(t=$v(e,i))||t.enumerable});return r},pe=r=>Bv(df(Hi(r!=null?qv(Mv(r)):{},"default",r&&r.__esModule&&"default"in r?{get:()=>r.default,enumerable:!0}:{value:r,enumerable:!0})),r);var m,u=P(()=>{m={platform:"",env:{},versions:{node:"14.17.6"}}});var Fv,be,ft=P(()=>{u();Fv=0,be={readFileSync:r=>self[r]||"",statSync:()=>({mtimeMs:Fv++}),promises:{readFile:r=>Promise.resolve(self[r]||"")}}});var Fs=x((oP,gf)=>{u();"use strict";var mf=class{constructor(e={}){if(!(e.maxSize&&e.maxSize>0))throw new TypeError("`maxSize` must be a number greater than 0");if(typeof e.maxAge=="number"&&e.maxAge===0)throw new TypeError("`maxAge` must be a number greater than 0");this.maxSize=e.maxSize,this.maxAge=e.maxAge||1/0,this.onEviction=e.onEviction,this.cache=new Map,this.oldCache=new Map,this._size=0}_emitEvictions(e){if(typeof this.onEviction=="function")for(let[t,i]of e)this.onEviction(t,i.value)}_deleteIfExpired(e,t){return typeof t.expiry=="number"&&t.expiry<=Date.now()?(typeof this.onEviction=="function"&&this.onEviction(e,t.value),this.delete(e)):!1}_getOrDeleteIfExpired(e,t){if(this._deleteIfExpired(e,t)===!1)return t.value}_getItemValue(e,t){return t.expiry?this._getOrDeleteIfExpired(e,t):t.value}_peek(e,t){let i=t.get(e);return this._getItemValue(e,i)}_set(e,t){this.cache.set(e,t),this._size++,this._size>=this.maxSize&&(this._size=0,this._emitEvictions(this.oldCache),this.oldCache=this.cache,this.cache=new Map)}_moveToRecent(e,t){this.oldCache.delete(e),this._set(e,t)}*_entriesAscending(){for(let e of this.oldCache){let[t,i]=e;this.cache.has(t)||this._deleteIfExpired(t,i)===!1&&(yield e)}for(let e of this.cache){let[t,i]=e;this._deleteIfExpired(t,i)===!1&&(yield e)}}get(e){if(this.cache.has(e)){let t=this.cache.get(e);return this._getItemValue(e,t)}if(this.oldCache.has(e)){let t=this.oldCache.get(e);if(this._deleteIfExpired(e,t)===!1)return this._moveToRecent(e,t),t.value}}set(e,t,{maxAge:i=this.maxAge===1/0?void 0:Date.now()+this.maxAge}={}){this.cache.has(e)?this.cache.set(e,{value:t,maxAge:i}):this._set(e,{value:t,expiry:i})}has(e){return this.cache.has(e)?!this._deleteIfExpired(e,this.cache.get(e)):this.oldCache.has(e)?!this._deleteIfExpired(e,this.oldCache.get(e)):!1}peek(e){if(this.cache.has(e))return this._peek(e,this.cache);if(this.oldCache.has(e))return this._peek(e,this.oldCache)}delete(e){let t=this.cache.delete(e);return t&&this._size--,this.oldCache.delete(e)||t}clear(){this.cache.clear(),this.oldCache.clear(),this._size=0}resize(e){if(!(e&&e>0))throw new TypeError("`maxSize` must be a number greater than 0");let t=[...this._entriesAscending()],i=t.length-e;i<0?(this.cache=new Map(t),this.oldCache=new Map,this._size=t.length):(i>0&&this._emitEvictions(t.slice(0,i)),this.oldCache=new Map(t.slice(i)),this.cache=new Map,this._size=0),this.maxSize=e}*keys(){for(let[e]of this)yield e}*values(){for(let[,e]of this)yield e}*[Symbol.iterator](){for(let e of this.cache){let[t,i]=e;this._deleteIfExpired(t,i)===!1&&(yield[t,i.value])}for(let e of this.oldCache){let[t,i]=e;this.cache.has(t)||this._deleteIfExpired(t,i)===!1&&(yield[t,i.value])}}*entriesDescending(){let e=[...this.cache];for(let t=e.length-1;t>=0;--t){let i=e[t],[n,s]=i;this._deleteIfExpired(n,s)===!1&&(yield[n,s.value])}e=[...this.oldCache];for(let t=e.length-1;t>=0;--t){let i=e[t],[n,s]=i;this.cache.has(n)||this._deleteIfExpired(n,s)===!1&&(yield[n,s.value])}}*entriesAscending(){for(let[e,t]of this._entriesAscending())yield[e,t.value]}get size(){if(!this._size)return this.oldCache.size;let e=0;for(let t of this.oldCache.keys())this.cache.has(t)||e++;return Math.min(this._size+e,this.maxSize)}};gf.exports=mf});var yf,bf=P(()=>{u();yf=r=>r&&r._hash});function Wi(r){return yf(r,{ignoreUnknown:!0})}var wf=P(()=>{u();bf()});function xt(r){if(r=`${r}`,r==="0")return"0";if(/^[+-]?(\d+|\d*\.\d+)(e[+-]?\d+)?(%|\w+)?$/.test(r))return r.replace(/^[+-]?/,t=>t==="-"?"":"-");let e=["var","calc","min","max","clamp"];for(let t of e)if(r.includes(`${t}(`))return`calc(${r} * -1)`}var Gi=P(()=>{u()});var vf,xf=P(()=>{u();vf=["preflight","container","accessibility","pointerEvents","visibility","position","inset","isolation","zIndex","order","gridColumn","gridColumnStart","gridColumnEnd","gridRow","gridRowStart","gridRowEnd","float","clear","margin","boxSizing","lineClamp","display","aspectRatio","size","height","maxHeight","minHeight","width","minWidth","maxWidth","flex","flexShrink","flexGrow","flexBasis","tableLayout","captionSide","borderCollapse","borderSpacing","transformOrigin","translate","rotate","skew","scale","transform","animation","cursor","touchAction","userSelect","resize","scrollSnapType","scrollSnapAlign","scrollSnapStop","scrollMargin","scrollPadding","listStylePosition","listStyleType","listStyleImage","appearance","columns","breakBefore","breakInside","breakAfter","gridAutoColumns","gridAutoFlow","gridAutoRows","gridTemplateColumns","gridTemplateRows","flexDirection","flexWrap","placeContent","placeItems","alignContent","alignItems","justifyContent","justifyItems","gap","space","divideWidth","divideStyle","divideColor","divideOpacity","placeSelf","alignSelf","justifySelf","overflow","overscrollBehavior","scrollBehavior","textOverflow","hyphens","whitespace","textWrap","wordBreak","borderRadius","borderWidth","borderStyle","borderColor","borderOpacity","backgroundColor","backgroundOpacity","backgroundImage","gradientColorStops","boxDecorationBreak","backgroundSize","backgroundAttachment","backgroundClip","backgroundPosition","backgroundRepeat","backgroundOrigin","fill","stroke","strokeWidth","objectFit","objectPosition","padding","textAlign","textIndent","verticalAlign","fontFamily","fontSize","fontWeight","textTransform","fontStyle","fontVariantNumeric","lineHeight","letterSpacing","textColor","textOpacity","textDecoration","textDecorationColor","textDecorationStyle","textDecorationThickness","textUnderlineOffset","fontSmoothing","placeholderColor","placeholderOpacity","caretColor","accentColor","opacity","backgroundBlendMode","mixBlendMode","boxShadow","boxShadowColor","outlineStyle","outlineWidth","outlineOffset","outlineColor","ringWidth","ringColor","ringOpacity","ringOffsetWidth","ringOffsetColor","blur","brightness","contrast","dropShadow","grayscale","hueRotate","invert","saturate","sepia","filter","backdropBlur","backdropBrightness","backdropContrast","backdropGrayscale","backdropHueRotate","backdropInvert","backdropOpacity","backdropSaturate","backdropSepia","backdropFilter","transitionProperty","transitionDelay","transitionDuration","transitionTimingFunction","willChange","contain","content","forcedColorAdjust"]});function kf(r,e){return r===void 0?e:Array.isArray(r)?r:[...new Set(e.filter(i=>r!==!1&&r[i]!==!1).concat(Object.keys(r).filter(i=>r[i]!==!1)))]}var Sf=P(()=>{u()});var Af={};Ge(Af,{default:()=>Qe});var Qe,Qi=P(()=>{u();Qe=new Proxy({},{get:()=>String})});function js(r,e,t){typeof m!="undefined"&&m.env.JEST_WORKER_ID||t&&Cf.has(t)||(t&&Cf.add(t),console.warn(""),e.forEach(i=>console.warn(r,"-",i)))}function zs(r){return Qe.dim(r)}var Cf,G,Be=P(()=>{u();Qi();Cf=new Set;G={info(r,e){js(Qe.bold(Qe.cyan("info")),...Array.isArray(r)?[r]:[e,r])},warn(r,e){["content-problems"].includes(r)||js(Qe.bold(Qe.yellow("warn")),...Array.isArray(r)?[r]:[e,r])},risk(r,e){js(Qe.bold(Qe.magenta("risk")),...Array.isArray(r)?[r]:[e,r])}}});var _f={};Ge(_f,{default:()=>Us});function qr({version:r,from:e,to:t}){G.warn(`${e}-color-renamed`,[`As of Tailwind CSS ${r}, \`${e}\` has been renamed to \`${t}\`.`,"Update your configuration file to silence this warning."])}var Us,Vs=P(()=>{u();Be();Us={inherit:"inherit",current:"currentColor",transparent:"transparent",black:"#000",white:"#fff",slate:{50:"#f8fafc",100:"#f1f5f9",200:"#e2e8f0",300:"#cbd5e1",400:"#94a3b8",500:"#64748b",600:"#475569",700:"#334155",800:"#1e293b",900:"#0f172a",950:"#020617"},gray:{50:"#f9fafb",100:"#f3f4f6",200:"#e5e7eb",300:"#d1d5db",400:"#9ca3af",500:"#6b7280",600:"#4b5563",700:"#374151",800:"#1f2937",900:"#111827",950:"#030712"},zinc:{50:"#fafafa",100:"#f4f4f5",200:"#e4e4e7",300:"#d4d4d8",400:"#a1a1aa",500:"#71717a",600:"#52525b",700:"#3f3f46",800:"#27272a",900:"#18181b",950:"#09090b"},neutral:{50:"#fafafa",100:"#f5f5f5",200:"#e5e5e5",300:"#d4d4d4",400:"#a3a3a3",500:"#737373",600:"#525252",700:"#404040",800:"#262626",900:"#171717",950:"#0a0a0a"},stone:{50:"#fafaf9",100:"#f5f5f4",200:"#e7e5e4",300:"#d6d3d1",400:"#a8a29e",500:"#78716c",600:"#57534e",700:"#44403c",800:"#292524",900:"#1c1917",950:"#0c0a09"},red:{50:"#fef2f2",100:"#fee2e2",200:"#fecaca",300:"#fca5a5",400:"#f87171",500:"#ef4444",600:"#dc2626",700:"#b91c1c",800:"#991b1b",900:"#7f1d1d",950:"#450a0a"},orange:{50:"#fff7ed",100:"#ffedd5",200:"#fed7aa",300:"#fdba74",400:"#fb923c",500:"#f97316",600:"#ea580c",700:"#c2410c",800:"#9a3412",900:"#7c2d12",950:"#431407"},amber:{50:"#fffbeb",100:"#fef3c7",200:"#fde68a",300:"#fcd34d",400:"#fbbf24",500:"#f59e0b",600:"#d97706",700:"#b45309",800:"#92400e",900:"#78350f",950:"#451a03"},yellow:{50:"#fefce8",100:"#fef9c3",200:"#fef08a",300:"#fde047",400:"#facc15",500:"#eab308",600:"#ca8a04",700:"#a16207",800:"#854d0e",900:"#713f12",950:"#422006"},lime:{50:"#f7fee7",100:"#ecfccb",200:"#d9f99d",300:"#bef264",400:"#a3e635",500:"#84cc16",600:"#65a30d",700:"#4d7c0f",800:"#3f6212",900:"#365314",950:"#1a2e05"},green:{50:"#f0fdf4",100:"#dcfce7",200:"#bbf7d0",300:"#86efac",400:"#4ade80",500:"#22c55e",600:"#16a34a",700:"#15803d",800:"#166534",900:"#14532d",950:"#052e16"},emerald:{50:"#ecfdf5",100:"#d1fae5",200:"#a7f3d0",300:"#6ee7b7",400:"#34d399",500:"#10b981",600:"#059669",700:"#047857",800:"#065f46",900:"#064e3b",950:"#022c22"},teal:{50:"#f0fdfa",100:"#ccfbf1",200:"#99f6e4",300:"#5eead4",400:"#2dd4bf",500:"#14b8a6",600:"#0d9488",700:"#0f766e",800:"#115e59",900:"#134e4a",950:"#042f2e"},cyan:{50:"#ecfeff",100:"#cffafe",200:"#a5f3fc",300:"#67e8f9",400:"#22d3ee",500:"#06b6d4",600:"#0891b2",700:"#0e7490",800:"#155e75",900:"#164e63",950:"#083344"},sky:{50:"#f0f9ff",100:"#e0f2fe",200:"#bae6fd",300:"#7dd3fc",400:"#38bdf8",500:"#0ea5e9",600:"#0284c7",700:"#0369a1",800:"#075985",900:"#0c4a6e",950:"#082f49"},blue:{50:"#eff6ff",100:"#dbeafe",200:"#bfdbfe",300:"#93c5fd",400:"#60a5fa",500:"#3b82f6",600:"#2563eb",700:"#1d4ed8",800:"#1e40af",900:"#1e3a8a",950:"#172554"},indigo:{50:"#eef2ff",100:"#e0e7ff",200:"#c7d2fe",300:"#a5b4fc",400:"#818cf8",500:"#6366f1",600:"#4f46e5",700:"#4338ca",800:"#3730a3",900:"#312e81",950:"#1e1b4b"},violet:{50:"#f5f3ff",100:"#ede9fe",200:"#ddd6fe",300:"#c4b5fd",400:"#a78bfa",500:"#8b5cf6",600:"#7c3aed",700:"#6d28d9",800:"#5b21b6",900:"#4c1d95",950:"#2e1065"},purple:{50:"#faf5ff",100:"#f3e8ff",200:"#e9d5ff",300:"#d8b4fe",400:"#c084fc",500:"#a855f7",600:"#9333ea",700:"#7e22ce",800:"#6b21a8",900:"#581c87",950:"#3b0764"},fuchsia:{50:"#fdf4ff",100:"#fae8ff",200:"#f5d0fe",300:"#f0abfc",400:"#e879f9",500:"#d946ef",600:"#c026d3",700:"#a21caf",800:"#86198f",900:"#701a75",950:"#4a044e"},pink:{50:"#fdf2f8",100:"#fce7f3",200:"#fbcfe8",300:"#f9a8d4",400:"#f472b6",500:"#ec4899",600:"#db2777",700:"#be185d",800:"#9d174d",900:"#831843",950:"#500724"},rose:{50:"#fff1f2",100:"#ffe4e6",200:"#fecdd3",300:"#fda4af",400:"#fb7185",500:"#f43f5e",600:"#e11d48",700:"#be123c",800:"#9f1239",900:"#881337",950:"#4c0519"},get lightBlue(){return qr({version:"v2.2",from:"lightBlue",to:"sky"}),this.sky},get warmGray(){return qr({version:"v3.0",from:"warmGray",to:"stone"}),this.stone},get trueGray(){return qr({version:"v3.0",from:"trueGray",to:"neutral"}),this.neutral},get coolGray(){return qr({version:"v3.0",from:"coolGray",to:"gray"}),this.gray},get blueGray(){return qr({version:"v3.0",from:"blueGray",to:"slate"}),this.slate}}});function Hs(r,...e){for(let t of e){for(let i in t)r?.hasOwnProperty?.(i)||(r[i]=t[i]);for(let i of Object.getOwnPropertySymbols(t))r?.hasOwnProperty?.(i)||(r[i]=t[i])}return r}var Ef=P(()=>{u()});function kt(r){if(Array.isArray(r))return r;let e=r.split("[").length-1,t=r.split("]").length-1;if(e!==t)throw new Error(`Path is invalid. Has unbalanced brackets: ${r}`);return r.split(/\.(?![^\[]*\])|[\[\]]/g).filter(Boolean)}var Yi=P(()=>{u()});function we(r,e){return Ki.future.includes(e)?r.future==="all"||(r?.future?.[e]??Of[e]??!1):Ki.experimental.includes(e)?r.experimental==="all"||(r?.experimental?.[e]??Of[e]??!1):!1}function Tf(r){return r.experimental==="all"?Ki.experimental:Object.keys(r?.experimental??{}).filter(e=>Ki.experimental.includes(e)&&r.experimental[e])}function Rf(r){if(m.env.JEST_WORKER_ID===void 0&&Tf(r).length>0){let e=Tf(r).map(t=>Qe.yellow(t)).join(", ");G.warn("experimental-flags-enabled",[`You have enabled experimental features: ${e}`,"Experimental features in Tailwind CSS are not covered by semver, may introduce breaking changes, and can change at any time."])}}var Of,Ki,ct=P(()=>{u();Qi();Be();Of={optimizeUniversalDefaults:!1,generalizedModifiers:!0,disableColorOpacityUtilitiesByDefault:!1,relativeContentPathsByDefault:!1},Ki={future:["hoverOnlyWhenSupported","respectDefaultRingColorOpacity","disableColorOpacityUtilitiesByDefault","relativeContentPathsByDefault"],experimental:["optimizeUniversalDefaults","generalizedModifiers"]}});function Pf(r){(()=>{if(r.purge||!r.content||!Array.isArray(r.content)&&!(typeof r.content=="object"&&r.content!==null))return!1;if(Array.isArray(r.content))return r.content.every(t=>typeof t=="string"?!0:!(typeof t?.raw!="string"||t?.extension&&typeof t?.extension!="string"));if(typeof r.content=="object"&&r.content!==null){if(Object.keys(r.content).some(t=>!["files","relative","extract","transform"].includes(t)))return!1;if(Array.isArray(r.content.files)){if(!r.content.files.every(t=>typeof t=="string"?!0:!(typeof t?.raw!="string"||t?.extension&&typeof t?.extension!="string")))return!1;if(typeof r.content.extract=="object"){for(let t of Object.values(r.content.extract))if(typeof t!="function")return!1}else if(!(r.content.extract===void 0||typeof r.content.extract=="function"))return!1;if(typeof r.content.transform=="object"){for(let t of Object.values(r.content.transform))if(typeof t!="function")return!1}else if(!(r.content.transform===void 0||typeof r.content.transform=="function"))return!1;if(typeof r.content.relative!="boolean"&&typeof r.content.relative!="undefined")return!1}return!0}return!1})()||G.warn("purge-deprecation",["The `purge`/`content` options have changed in Tailwind CSS v3.0.","Update your configuration file to eliminate this warning.","https://tailwindcss.com/docs/upgrade-guide#configure-content-sources"]),r.safelist=(()=>{let{content:t,purge:i,safelist:n}=r;return Array.isArray(n)?n:Array.isArray(t?.safelist)?t.safelist:Array.isArray(i?.safelist)?i.safelist:Array.isArray(i?.options?.safelist)?i.options.safelist:[]})(),r.blocklist=(()=>{let{blocklist:t}=r;if(Array.isArray(t)){if(t.every(i=>typeof i=="string"))return t;G.warn("blocklist-invalid",["The `blocklist` option must be an array of strings.","https://tailwindcss.com/docs/content-configuration#discarding-classes"])}return[]})(),typeof r.prefix=="function"?(G.warn("prefix-function",["As of Tailwind CSS v3.0, `prefix` cannot be a function.","Update `prefix` in your configuration to be a string to eliminate this warning.","https://tailwindcss.com/docs/upgrade-guide#prefix-cannot-be-a-function"]),r.prefix=""):r.prefix=r.prefix??"",r.content={relative:(()=>{let{content:t}=r;return t?.relative?t.relative:we(r,"relativeContentPathsByDefault")})(),files:(()=>{let{content:t,purge:i}=r;return Array.isArray(i)?i:Array.isArray(i?.content)?i.content:Array.isArray(t)?t:Array.isArray(t?.content)?t.content:Array.isArray(t?.files)?t.files:[]})(),extract:(()=>{let t=(()=>r.purge?.extract?r.purge.extract:r.content?.extract?r.content.extract:r.purge?.extract?.DEFAULT?r.purge.extract.DEFAULT:r.content?.extract?.DEFAULT?r.content.extract.DEFAULT:r.purge?.options?.extractors?r.purge.options.extractors:r.content?.options?.extractors?r.content.options.extractors:{})(),i={},n=(()=>{if(r.purge?.options?.defaultExtractor)return r.purge.options.defaultExtractor;if(r.content?.options?.defaultExtractor)return r.content.options.defaultExtractor})();if(n!==void 0&&(i.DEFAULT=n),typeof t=="function")i.DEFAULT=t;else if(Array.isArray(t))for(let{extensions:s,extractor:a}of t??[])for(let o of s)i[o]=a;else typeof t=="object"&&t!==null&&Object.assign(i,t);return i})(),transform:(()=>{let t=(()=>r.purge?.transform?r.purge.transform:r.content?.transform?r.content.transform:r.purge?.transform?.DEFAULT?r.purge.transform.DEFAULT:r.content?.transform?.DEFAULT?r.content.transform.DEFAULT:{})(),i={};return typeof t=="function"?i.DEFAULT=t:typeof t=="object"&&t!==null&&Object.assign(i,t),i})()};for(let t of r.content.files)if(typeof t=="string"&&/{([^,]*?)}/g.test(t)){G.warn("invalid-glob-braces",[`The glob pattern ${zs(t)} in your Tailwind CSS configuration is invalid.`,`Update it to ${zs(t.replace(/{([^,]*?)}/g,"$1"))} to silence this warning.`]);break}return r}var If=P(()=>{u();ct();Be()});function ke(r){if(Object.prototype.toString.call(r)!=="[object Object]")return!1;let e=Object.getPrototypeOf(r);return e===null||Object.getPrototypeOf(e)===null}var Kt=P(()=>{u()});function St(r){return Array.isArray(r)?r.map(e=>St(e)):typeof r=="object"&&r!==null?Object.fromEntries(Object.entries(r).map(([e,t])=>[e,St(t)])):r}var Xi=P(()=>{u()});function jt(r){return r.replace(/\\,/g,"\\2c ")}var Zi=P(()=>{u()});var Ws,Df=P(()=>{u();Ws={aliceblue:[240,248,255],antiquewhite:[250,235,215],aqua:[0,255,255],aquamarine:[127,255,212],azure:[240,255,255],beige:[245,245,220],bisque:[255,228,196],black:[0,0,0],blanchedalmond:[255,235,205],blue:[0,0,255],blueviolet:[138,43,226],brown:[165,42,42],burlywood:[222,184,135],cadetblue:[95,158,160],chartreuse:[127,255,0],chocolate:[210,105,30],coral:[255,127,80],cornflowerblue:[100,149,237],cornsilk:[255,248,220],crimson:[220,20,60],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgoldenrod:[184,134,11],darkgray:[169,169,169],darkgreen:[0,100,0],darkgrey:[169,169,169],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkseagreen:[143,188,143],darkslateblue:[72,61,139],darkslategray:[47,79,79],darkslategrey:[47,79,79],darkturquoise:[0,206,209],darkviolet:[148,0,211],deeppink:[255,20,147],deepskyblue:[0,191,255],dimgray:[105,105,105],dimgrey:[105,105,105],dodgerblue:[30,144,255],firebrick:[178,34,34],floralwhite:[255,250,240],forestgreen:[34,139,34],fuchsia:[255,0,255],gainsboro:[220,220,220],ghostwhite:[248,248,255],gold:[255,215,0],goldenrod:[218,165,32],gray:[128,128,128],green:[0,128,0],greenyellow:[173,255,47],grey:[128,128,128],honeydew:[240,255,240],hotpink:[255,105,180],indianred:[205,92,92],indigo:[75,0,130],ivory:[255,255,240],khaki:[240,230,140],lavender:[230,230,250],lavenderblush:[255,240,245],lawngreen:[124,252,0],lemonchiffon:[255,250,205],lightblue:[173,216,230],lightcoral:[240,128,128],lightcyan:[224,255,255],lightgoldenrodyellow:[250,250,210],lightgray:[211,211,211],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightsalmon:[255,160,122],lightseagreen:[32,178,170],lightskyblue:[135,206,250],lightslategray:[119,136,153],lightslategrey:[119,136,153],lightsteelblue:[176,196,222],lightyellow:[255,255,224],lime:[0,255,0],limegreen:[50,205,50],linen:[250,240,230],magenta:[255,0,255],maroon:[128,0,0],mediumaquamarine:[102,205,170],mediumblue:[0,0,205],mediumorchid:[186,85,211],mediumpurple:[147,112,219],mediumseagreen:[60,179,113],mediumslateblue:[123,104,238],mediumspringgreen:[0,250,154],mediumturquoise:[72,209,204],mediumvioletred:[199,21,133],midnightblue:[25,25,112],mintcream:[245,255,250],mistyrose:[255,228,225],moccasin:[255,228,181],navajowhite:[255,222,173],navy:[0,0,128],oldlace:[253,245,230],olive:[128,128,0],olivedrab:[107,142,35],orange:[255,165,0],orangered:[255,69,0],orchid:[218,112,214],palegoldenrod:[238,232,170],palegreen:[152,251,152],paleturquoise:[175,238,238],palevioletred:[219,112,147],papayawhip:[255,239,213],peachpuff:[255,218,185],peru:[205,133,63],pink:[255,192,203],plum:[221,160,221],powderblue:[176,224,230],purple:[128,0,128],rebeccapurple:[102,51,153],red:[255,0,0],rosybrown:[188,143,143],royalblue:[65,105,225],saddlebrown:[139,69,19],salmon:[250,128,114],sandybrown:[244,164,96],seagreen:[46,139,87],seashell:[255,245,238],sienna:[160,82,45],silver:[192,192,192],skyblue:[135,206,235],slateblue:[106,90,205],slategray:[112,128,144],slategrey:[112,128,144],snow:[255,250,250],springgreen:[0,255,127],steelblue:[70,130,180],tan:[210,180,140],teal:[0,128,128],thistle:[216,191,216],tomato:[255,99,71],turquoise:[64,224,208],violet:[238,130,238],wheat:[245,222,179],white:[255,255,255],whitesmoke:[245,245,245],yellow:[255,255,0],yellowgreen:[154,205,50]}});function $r(r,{loose:e=!1}={}){if(typeof r!="string")return null;if(r=r.trim(),r==="transparent")return{mode:"rgb",color:["0","0","0"],alpha:"0"};if(r in Ws)return{mode:"rgb",color:Ws[r].map(s=>s.toString())};let t=r.replace(zv,(s,a,o,l,c)=>["#",a,a,o,o,l,l,c?c+c:""].join("")).match(jv);if(t!==null)return{mode:"rgb",color:[parseInt(t[1],16),parseInt(t[2],16),parseInt(t[3],16)].map(s=>s.toString()),alpha:t[4]?(parseInt(t[4],16)/255).toString():void 0};let i=r.match(Uv)??r.match(Vv);if(i===null)return null;let n=[i[2],i[3],i[4]].filter(Boolean).map(s=>s.toString());return n.length===2&&n[0].startsWith("var(")?{mode:i[1],color:[n[0]],alpha:n[1]}:!e&&n.length!==3||n.length<3&&!n.some(s=>/^var\(.*?\)$/.test(s))?null:{mode:i[1],color:n,alpha:i[5]?.toString?.()}}function Gs({mode:r,color:e,alpha:t}){let i=t!==void 0;return r==="rgba"||r==="hsla"?`${r}(${e.join(", ")}${i?`, ${t}`:""})`:`${r}(${e.join(" ")}${i?` / ${t}`:""})`}var jv,zv,At,Ji,qf,Ct,Uv,Vv,Qs=P(()=>{u();Df();jv=/^#([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})?$/i,zv=/^#([a-f\d])([a-f\d])([a-f\d])([a-f\d])?$/i,At=/(?:\d+|\d*\.\d+)%?/,Ji=/(?:\s*,\s*|\s+)/,qf=/\s*[,/]\s*/,Ct=/var\(--(?:[^ )]*?)(?:,(?:[^ )]*?|var\(--[^ )]*?\)))?\)/,Uv=new RegExp(`^(rgba?)\\(\\s*(${At.source}|${Ct.source})(?:${Ji.source}(${At.source}|${Ct.source}))?(?:${Ji.source}(${At.source}|${Ct.source}))?(?:${qf.source}(${At.source}|${Ct.source}))?\\s*\\)$`),Vv=new RegExp(`^(hsla?)\\(\\s*((?:${At.source})(?:deg|rad|grad|turn)?|${Ct.source})(?:${Ji.source}(${At.source}|${Ct.source}))?(?:${Ji.source}(${At.source}|${Ct.source}))?(?:${qf.source}(${At.source}|${Ct.source}))?\\s*\\)$`)});function Je(r,e,t){if(typeof r=="function")return r({opacityValue:e});let i=$r(r,{loose:!0});return i===null?t:Gs({...i,alpha:e})}function Ae({color:r,property:e,variable:t}){let i=[].concat(e);if(typeof r=="function")return{[t]:"1",...Object.fromEntries(i.map(s=>[s,r({opacityVariable:t,opacityValue:`var(${t}, 1)`})]))};let n=$r(r);return n===null?Object.fromEntries(i.map(s=>[s,r])):n.alpha!==void 0?Object.fromEntries(i.map(s=>[s,r])):{[t]:"1",...Object.fromEntries(i.map(s=>[s,Gs({...n,alpha:`var(${t}, 1)`})]))}}var Lr=P(()=>{u();Qs()});function ve(r,e){let t=[],i=[],n=0,s=!1;for(let a=0;a{u()});function en(r){return ve(r,",").map(t=>{let i=t.trim(),n={raw:i},s=i.split(Wv),a=new Set;for(let o of s)$f.lastIndex=0,!a.has("KEYWORD")&&Hv.has(o)?(n.keyword=o,a.add("KEYWORD")):$f.test(o)?a.has("X")?a.has("Y")?a.has("BLUR")?a.has("SPREAD")||(n.spread=o,a.add("SPREAD")):(n.blur=o,a.add("BLUR")):(n.y=o,a.add("Y")):(n.x=o,a.add("X")):n.color?(n.unknown||(n.unknown=[]),n.unknown.push(o)):n.color=o;return n.valid=n.x!==void 0&&n.y!==void 0,n})}function Lf(r){return r.map(e=>e.valid?[e.keyword,e.x,e.y,e.blur,e.spread,e.color].filter(Boolean).join(" "):e.raw).join(", ")}var Hv,Wv,$f,Ys=P(()=>{u();zt();Hv=new Set(["inset","inherit","initial","revert","unset"]),Wv=/\ +(?![^(]*\))/g,$f=/^-?(\d+|\.\d+)(.*?)$/g});function Ks(r){return Gv.some(e=>new RegExp(`^${e}\\(.*\\)`).test(r))}function K(r,e=null,t=!0){let i=e&&Qv.has(e.property);return r.startsWith("--")&&!i?`var(${r})`:r.includes("url(")?r.split(/(url\(.*?\))/g).filter(Boolean).map(n=>/^url\(.*?\)$/.test(n)?n:K(n,e,!1)).join(""):(r=r.replace(/([^\\])_+/g,(n,s)=>s+" ".repeat(n.length-1)).replace(/^_/g," ").replace(/\\_/g,"_"),t&&(r=r.trim()),r=Yv(r),r)}function Ye(r){return r.includes("=")&&(r=r.replace(/(=.*)/g,(e,t)=>{if(t[1]==="'"||t[1]==='"')return t;if(t.length>2){let i=t[t.length-1];if(t[t.length-2]===" "&&(i==="i"||i==="I"||i==="s"||i==="S"))return`="${t.slice(1,-2)}" ${t[t.length-1]}`}return`="${t.slice(1)}"`})),r}function Yv(r){let e=["theme"],t=["min-content","max-content","fit-content","safe-area-inset-top","safe-area-inset-right","safe-area-inset-bottom","safe-area-inset-left","titlebar-area-x","titlebar-area-y","titlebar-area-width","titlebar-area-height","keyboard-inset-top","keyboard-inset-right","keyboard-inset-bottom","keyboard-inset-left","keyboard-inset-width","keyboard-inset-height","radial-gradient","linear-gradient","conic-gradient","repeating-radial-gradient","repeating-linear-gradient","repeating-conic-gradient","anchor-size"];return r.replace(/(calc|min|max|clamp)\(.+\)/g,i=>{let n="";function s(){let a=n.trimEnd();return a[a.length-1]}for(let a=0;ai[a+p]===d)},l=function(f){let d=1/0;for(let h of f){let b=i.indexOf(h,a);b!==-1&&bo(f))){let f=t.find(d=>o(d));n+=f,a+=f.length-1}else e.some(f=>o(f))?n+=l([")"]):o("[")?n+=l(["]"]):["+","-","*","/"].includes(c)&&!["(","+","-","*","/",","].includes(s())?n+=` ${c} `:n+=c}return n.replace(/\s+/g," ")})}function Xs(r){return r.startsWith("url(")}function Zs(r){return!isNaN(Number(r))||Ks(r)}function Mr(r){return r.endsWith("%")&&Zs(r.slice(0,-1))||Ks(r)}function Nr(r){return r==="0"||new RegExp(`^[+-]?[0-9]*.?[0-9]+(?:[eE][+-]?[0-9]+)?${Xv}$`).test(r)||Ks(r)}function Mf(r){return Zv.has(r)}function Nf(r){let e=en(K(r));for(let t of e)if(!t.valid)return!1;return!0}function Bf(r){let e=0;return ve(r,"_").every(i=>(i=K(i),i.startsWith("var(")?!0:$r(i,{loose:!0})!==null?(e++,!0):!1))?e>0:!1}function Ff(r){let e=0;return ve(r,",").every(i=>(i=K(i),i.startsWith("var(")?!0:Xs(i)||ex(i)||["element(","image(","cross-fade(","image-set("].some(n=>i.startsWith(n))?(e++,!0):!1))?e>0:!1}function ex(r){r=K(r);for(let e of Jv)if(r.startsWith(`${e}(`))return!0;return!1}function jf(r){let e=0;return ve(r,"_").every(i=>(i=K(i),i.startsWith("var(")?!0:tx.has(i)||Nr(i)||Mr(i)?(e++,!0):!1))?e>0:!1}function zf(r){let e=0;return ve(r,",").every(i=>(i=K(i),i.startsWith("var(")?!0:i.includes(" ")&&!/(['"])([^"']+)\1/g.test(i)||/^\d/g.test(i)?!1:(e++,!0)))?e>0:!1}function Uf(r){return rx.has(r)}function Vf(r){return ix.has(r)}function Hf(r){return nx.has(r)}var Gv,Qv,Kv,Xv,Zv,Jv,tx,rx,ix,nx,Br=P(()=>{u();Qs();Ys();zt();Gv=["min","max","clamp","calc"];Qv=new Set(["scroll-timeline-name","timeline-scope","view-timeline-name","font-palette","anchor-name","anchor-scope","position-anchor","position-try-options","scroll-timeline","animation-timeline","view-timeline","position-try"]);Kv=["cm","mm","Q","in","pc","pt","px","em","ex","ch","rem","lh","rlh","vw","vh","vmin","vmax","vb","vi","svw","svh","lvw","lvh","dvw","dvh","cqw","cqh","cqi","cqb","cqmin","cqmax"],Xv=`(?:${Kv.join("|")})`;Zv=new Set(["thin","medium","thick"]);Jv=new Set(["conic-gradient","linear-gradient","radial-gradient","repeating-conic-gradient","repeating-linear-gradient","repeating-radial-gradient"]);tx=new Set(["center","top","right","bottom","left"]);rx=new Set(["serif","sans-serif","monospace","cursive","fantasy","system-ui","ui-serif","ui-sans-serif","ui-monospace","ui-rounded","math","emoji","fangsong"]);ix=new Set(["xx-small","x-small","small","medium","large","x-large","xx-large","xxx-large"]);nx=new Set(["larger","smaller"])});function Wf(r){let e=["cover","contain"];return ve(r,",").every(t=>{let i=ve(t,"_").filter(Boolean);return i.length===1&&e.includes(i[0])?!0:i.length!==1&&i.length!==2?!1:i.every(n=>Nr(n)||Mr(n)||n==="auto")})}var Gf=P(()=>{u();Br();zt()});function Qf(r,e){r.walkClasses(t=>{t.value=e(t.value),t.raws&&t.raws.value&&(t.raws.value=jt(t.raws.value))})}function Yf(r,e){if(!_t(r))return;let t=r.slice(1,-1);if(!!e(t))return K(t)}function sx(r,e={},t){let i=e[r];if(i!==void 0)return xt(i);if(_t(r)){let n=Yf(r,t);return n===void 0?void 0:xt(n)}}function tn(r,e={},{validate:t=()=>!0}={}){let i=e.values?.[r];return i!==void 0?i:e.supportsNegativeValues&&r.startsWith("-")?sx(r.slice(1),e.values,t):Yf(r,t)}function _t(r){return r.startsWith("[")&&r.endsWith("]")}function Kf(r){let e=r.lastIndexOf("/"),t=r.lastIndexOf("[",e),i=r.indexOf("]",e);return r[e-1]==="]"||r[e+1]==="["||t!==-1&&i!==-1&&t")){let e=r;return({opacityValue:t=1})=>e.replace(//g,t)}return r}function Xf(r){return K(r.slice(1,-1))}function ax(r,e={},{tailwindConfig:t={}}={}){if(e.values?.[r]!==void 0)return Xt(e.values?.[r]);let[i,n]=Kf(r);if(n!==void 0){let s=e.values?.[i]??(_t(i)?i.slice(1,-1):void 0);return s===void 0?void 0:(s=Xt(s),_t(n)?Je(s,Xf(n)):t.theme?.opacity?.[n]===void 0?void 0:Je(s,t.theme.opacity[n]))}return tn(r,e,{validate:Bf})}function ox(r,e={}){return e.values?.[r]}function qe(r){return(e,t)=>tn(e,t,{validate:r})}function lx(r,e){let t=r.indexOf(e);return t===-1?[void 0,r]:[r.slice(0,t),r.slice(t+1)]}function ea(r,e,t,i){if(t.values&&e in t.values)for(let{type:s}of r??[]){let a=Js[s](e,t,{tailwindConfig:i});if(a!==void 0)return[a,s,null]}if(_t(e)){let s=e.slice(1,-1),[a,o]=lx(s,":");if(!/^[\w-_]+$/g.test(a))o=s;else if(a!==void 0&&!Zf.includes(a))return[];if(o.length>0&&Zf.includes(a))return[tn(`[${o}]`,t),a,null]}let n=ta(r,e,t,i);for(let s of n)return s;return[]}function*ta(r,e,t,i){let n=we(i,"generalizedModifiers"),[s,a]=Kf(e);if(n&&t.modifiers!=null&&(t.modifiers==="any"||typeof t.modifiers=="object"&&(a&&_t(a)||a in t.modifiers))||(s=e,a=void 0),a!==void 0&&s===""&&(s="DEFAULT"),a!==void 0&&typeof t.modifiers=="object"){let l=t.modifiers?.[a]??null;l!==null?a=l:_t(a)&&(a=Xf(a))}for(let{type:l}of r??[]){let c=Js[l](s,t,{tailwindConfig:i});c!==void 0&&(yield[c,l,a??null])}}var Js,Zf,Fr=P(()=>{u();Zi();Lr();Br();Gi();Gf();ct();Js={any:tn,color:ax,url:qe(Xs),image:qe(Ff),length:qe(Nr),percentage:qe(Mr),position:qe(jf),lookup:ox,"generic-name":qe(Uf),"family-name":qe(zf),number:qe(Zs),"line-width":qe(Mf),"absolute-size":qe(Vf),"relative-size":qe(Hf),shadow:qe(Nf),size:qe(Wf)},Zf=Object.keys(Js)});function X(r){return typeof r=="function"?r({}):r}var ra=P(()=>{u()});function Zt(r){return typeof r=="function"}function jr(r,...e){let t=e.pop();for(let i of e)for(let n in i){let s=t(r[n],i[n]);s===void 0?ke(r[n])&&ke(i[n])?r[n]=jr({},r[n],i[n],t):r[n]=i[n]:r[n]=s}return r}function ux(r,...e){return Zt(r)?r(...e):r}function fx(r){return r.reduce((e,{extend:t})=>jr(e,t,(i,n)=>i===void 0?[n]:Array.isArray(i)?[n,...i]:[n,i]),{})}function cx(r){return{...r.reduce((e,t)=>Hs(e,t),{}),extend:fx(r)}}function Jf(r,e){if(Array.isArray(r)&&ke(r[0]))return r.concat(e);if(Array.isArray(e)&&ke(e[0])&&ke(r))return[r,...e];if(Array.isArray(e))return e}function px({extend:r,...e}){return jr(e,r,(t,i)=>!Zt(t)&&!i.some(Zt)?jr({},t,...i,Jf):(n,s)=>jr({},...[t,...i].map(a=>ux(a,n,s)),Jf))}function*dx(r){let e=kt(r);if(e.length===0||(yield e,Array.isArray(r)))return;let t=/^(.*?)\s*\/\s*([^/]+)$/,i=r.match(t);if(i!==null){let[,n,s]=i,a=kt(n);a.alpha=s,yield a}}function hx(r){let e=(t,i)=>{for(let n of dx(t)){let s=0,a=r;for(;a!=null&&s(t[i]=Zt(r[i])?r[i](e,ia):r[i],t),{})}function ec(r){let e=[];return r.forEach(t=>{e=[...e,t];let i=t?.plugins??[];i.length!==0&&i.forEach(n=>{n.__isOptionsFunction&&(n=n()),e=[...e,...ec([n?.config??{}])]})}),e}function mx(r){return[...r].reduceRight((t,i)=>Zt(i)?i({corePlugins:t}):kf(i,t),vf)}function gx(r){return[...r].reduceRight((t,i)=>[...t,...i],[])}function na(r){let e=[...ec(r),{prefix:"",important:!1,separator:":"}];return Pf(Hs({theme:hx(px(cx(e.map(t=>t?.theme??{})))),corePlugins:mx(e.map(t=>t.corePlugins)),plugins:gx(r.map(t=>t?.plugins??[]))},...e))}var ia,tc=P(()=>{u();Gi();xf();Sf();Vs();Ef();Yi();If();Kt();Xi();Fr();Lr();ra();ia={colors:Us,negative(r){return Object.keys(r).filter(e=>r[e]!=="0").reduce((e,t)=>{let i=xt(r[t]);return i!==void 0&&(e[`-${t}`]=i),e},{})},breakpoints(r){return Object.keys(r).filter(e=>typeof r[e]=="string").reduce((e,t)=>({...e,[`screen-${t}`]:r[t]}),{})}}});var rn=x((f3,rc)=>{u();rc.exports={content:[],presets:[],darkMode:"media",theme:{accentColor:({theme:r})=>({...r("colors"),auto:"auto"}),animation:{none:"none",spin:"spin 1s linear infinite",ping:"ping 1s cubic-bezier(0, 0, 0.2, 1) infinite",pulse:"pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite",bounce:"bounce 1s infinite"},aria:{busy:'busy="true"',checked:'checked="true"',disabled:'disabled="true"',expanded:'expanded="true"',hidden:'hidden="true"',pressed:'pressed="true"',readonly:'readonly="true"',required:'required="true"',selected:'selected="true"'},aspectRatio:{auto:"auto",square:"1 / 1",video:"16 / 9"},backdropBlur:({theme:r})=>r("blur"),backdropBrightness:({theme:r})=>r("brightness"),backdropContrast:({theme:r})=>r("contrast"),backdropGrayscale:({theme:r})=>r("grayscale"),backdropHueRotate:({theme:r})=>r("hueRotate"),backdropInvert:({theme:r})=>r("invert"),backdropOpacity:({theme:r})=>r("opacity"),backdropSaturate:({theme:r})=>r("saturate"),backdropSepia:({theme:r})=>r("sepia"),backgroundColor:({theme:r})=>r("colors"),backgroundImage:{none:"none","gradient-to-t":"linear-gradient(to top, var(--tw-gradient-stops))","gradient-to-tr":"linear-gradient(to top right, var(--tw-gradient-stops))","gradient-to-r":"linear-gradient(to right, var(--tw-gradient-stops))","gradient-to-br":"linear-gradient(to bottom right, var(--tw-gradient-stops))","gradient-to-b":"linear-gradient(to bottom, var(--tw-gradient-stops))","gradient-to-bl":"linear-gradient(to bottom left, var(--tw-gradient-stops))","gradient-to-l":"linear-gradient(to left, var(--tw-gradient-stops))","gradient-to-tl":"linear-gradient(to top left, var(--tw-gradient-stops))"},backgroundOpacity:({theme:r})=>r("opacity"),backgroundPosition:{bottom:"bottom",center:"center",left:"left","left-bottom":"left bottom","left-top":"left top",right:"right","right-bottom":"right bottom","right-top":"right top",top:"top"},backgroundSize:{auto:"auto",cover:"cover",contain:"contain"},blur:{0:"0",none:"",sm:"4px",DEFAULT:"8px",md:"12px",lg:"16px",xl:"24px","2xl":"40px","3xl":"64px"},borderColor:({theme:r})=>({...r("colors"),DEFAULT:r("colors.gray.200","currentColor")}),borderOpacity:({theme:r})=>r("opacity"),borderRadius:{none:"0px",sm:"0.125rem",DEFAULT:"0.25rem",md:"0.375rem",lg:"0.5rem",xl:"0.75rem","2xl":"1rem","3xl":"1.5rem",full:"9999px"},borderSpacing:({theme:r})=>({...r("spacing")}),borderWidth:{DEFAULT:"1px",0:"0px",2:"2px",4:"4px",8:"8px"},boxShadow:{sm:"0 1px 2px 0 rgb(0 0 0 / 0.05)",DEFAULT:"0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)",md:"0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",lg:"0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)",xl:"0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)","2xl":"0 25px 50px -12px rgb(0 0 0 / 0.25)",inner:"inset 0 2px 4px 0 rgb(0 0 0 / 0.05)",none:"none"},boxShadowColor:({theme:r})=>r("colors"),brightness:{0:"0",50:".5",75:".75",90:".9",95:".95",100:"1",105:"1.05",110:"1.1",125:"1.25",150:"1.5",200:"2"},caretColor:({theme:r})=>r("colors"),colors:({colors:r})=>({inherit:r.inherit,current:r.current,transparent:r.transparent,black:r.black,white:r.white,slate:r.slate,gray:r.gray,zinc:r.zinc,neutral:r.neutral,stone:r.stone,red:r.red,orange:r.orange,amber:r.amber,yellow:r.yellow,lime:r.lime,green:r.green,emerald:r.emerald,teal:r.teal,cyan:r.cyan,sky:r.sky,blue:r.blue,indigo:r.indigo,violet:r.violet,purple:r.purple,fuchsia:r.fuchsia,pink:r.pink,rose:r.rose}),columns:{auto:"auto",1:"1",2:"2",3:"3",4:"4",5:"5",6:"6",7:"7",8:"8",9:"9",10:"10",11:"11",12:"12","3xs":"16rem","2xs":"18rem",xs:"20rem",sm:"24rem",md:"28rem",lg:"32rem",xl:"36rem","2xl":"42rem","3xl":"48rem","4xl":"56rem","5xl":"64rem","6xl":"72rem","7xl":"80rem"},container:{},content:{none:"none"},contrast:{0:"0",50:".5",75:".75",100:"1",125:"1.25",150:"1.5",200:"2"},cursor:{auto:"auto",default:"default",pointer:"pointer",wait:"wait",text:"text",move:"move",help:"help","not-allowed":"not-allowed",none:"none","context-menu":"context-menu",progress:"progress",cell:"cell",crosshair:"crosshair","vertical-text":"vertical-text",alias:"alias",copy:"copy","no-drop":"no-drop",grab:"grab",grabbing:"grabbing","all-scroll":"all-scroll","col-resize":"col-resize","row-resize":"row-resize","n-resize":"n-resize","e-resize":"e-resize","s-resize":"s-resize","w-resize":"w-resize","ne-resize":"ne-resize","nw-resize":"nw-resize","se-resize":"se-resize","sw-resize":"sw-resize","ew-resize":"ew-resize","ns-resize":"ns-resize","nesw-resize":"nesw-resize","nwse-resize":"nwse-resize","zoom-in":"zoom-in","zoom-out":"zoom-out"},divideColor:({theme:r})=>r("borderColor"),divideOpacity:({theme:r})=>r("borderOpacity"),divideWidth:({theme:r})=>r("borderWidth"),dropShadow:{sm:"0 1px 1px rgb(0 0 0 / 0.05)",DEFAULT:["0 1px 2px rgb(0 0 0 / 0.1)","0 1px 1px rgb(0 0 0 / 0.06)"],md:["0 4px 3px rgb(0 0 0 / 0.07)","0 2px 2px rgb(0 0 0 / 0.06)"],lg:["0 10px 8px rgb(0 0 0 / 0.04)","0 4px 3px rgb(0 0 0 / 0.1)"],xl:["0 20px 13px rgb(0 0 0 / 0.03)","0 8px 5px rgb(0 0 0 / 0.08)"],"2xl":"0 25px 25px rgb(0 0 0 / 0.15)",none:"0 0 #0000"},fill:({theme:r})=>({none:"none",...r("colors")}),flex:{1:"1 1 0%",auto:"1 1 auto",initial:"0 1 auto",none:"none"},flexBasis:({theme:r})=>({auto:"auto",...r("spacing"),"1/2":"50%","1/3":"33.333333%","2/3":"66.666667%","1/4":"25%","2/4":"50%","3/4":"75%","1/5":"20%","2/5":"40%","3/5":"60%","4/5":"80%","1/6":"16.666667%","2/6":"33.333333%","3/6":"50%","4/6":"66.666667%","5/6":"83.333333%","1/12":"8.333333%","2/12":"16.666667%","3/12":"25%","4/12":"33.333333%","5/12":"41.666667%","6/12":"50%","7/12":"58.333333%","8/12":"66.666667%","9/12":"75%","10/12":"83.333333%","11/12":"91.666667%",full:"100%"}),flexGrow:{0:"0",DEFAULT:"1"},flexShrink:{0:"0",DEFAULT:"1"},fontFamily:{sans:["ui-sans-serif","system-ui","sans-serif",'"Apple Color Emoji"','"Segoe UI Emoji"','"Segoe UI Symbol"','"Noto Color Emoji"'],serif:["ui-serif","Georgia","Cambria",'"Times New Roman"',"Times","serif"],mono:["ui-monospace","SFMono-Regular","Menlo","Monaco","Consolas",'"Liberation Mono"','"Courier New"',"monospace"]},fontSize:{xs:["0.75rem",{lineHeight:"1rem"}],sm:["0.875rem",{lineHeight:"1.25rem"}],base:["1rem",{lineHeight:"1.5rem"}],lg:["1.125rem",{lineHeight:"1.75rem"}],xl:["1.25rem",{lineHeight:"1.75rem"}],"2xl":["1.5rem",{lineHeight:"2rem"}],"3xl":["1.875rem",{lineHeight:"2.25rem"}],"4xl":["2.25rem",{lineHeight:"2.5rem"}],"5xl":["3rem",{lineHeight:"1"}],"6xl":["3.75rem",{lineHeight:"1"}],"7xl":["4.5rem",{lineHeight:"1"}],"8xl":["6rem",{lineHeight:"1"}],"9xl":["8rem",{lineHeight:"1"}]},fontWeight:{thin:"100",extralight:"200",light:"300",normal:"400",medium:"500",semibold:"600",bold:"700",extrabold:"800",black:"900"},gap:({theme:r})=>r("spacing"),gradientColorStops:({theme:r})=>r("colors"),gradientColorStopPositions:{"0%":"0%","5%":"5%","10%":"10%","15%":"15%","20%":"20%","25%":"25%","30%":"30%","35%":"35%","40%":"40%","45%":"45%","50%":"50%","55%":"55%","60%":"60%","65%":"65%","70%":"70%","75%":"75%","80%":"80%","85%":"85%","90%":"90%","95%":"95%","100%":"100%"},grayscale:{0:"0",DEFAULT:"100%"},gridAutoColumns:{auto:"auto",min:"min-content",max:"max-content",fr:"minmax(0, 1fr)"},gridAutoRows:{auto:"auto",min:"min-content",max:"max-content",fr:"minmax(0, 1fr)"},gridColumn:{auto:"auto","span-1":"span 1 / span 1","span-2":"span 2 / span 2","span-3":"span 3 / span 3","span-4":"span 4 / span 4","span-5":"span 5 / span 5","span-6":"span 6 / span 6","span-7":"span 7 / span 7","span-8":"span 8 / span 8","span-9":"span 9 / span 9","span-10":"span 10 / span 10","span-11":"span 11 / span 11","span-12":"span 12 / span 12","span-full":"1 / -1"},gridColumnEnd:{auto:"auto",1:"1",2:"2",3:"3",4:"4",5:"5",6:"6",7:"7",8:"8",9:"9",10:"10",11:"11",12:"12",13:"13"},gridColumnStart:{auto:"auto",1:"1",2:"2",3:"3",4:"4",5:"5",6:"6",7:"7",8:"8",9:"9",10:"10",11:"11",12:"12",13:"13"},gridRow:{auto:"auto","span-1":"span 1 / span 1","span-2":"span 2 / span 2","span-3":"span 3 / span 3","span-4":"span 4 / span 4","span-5":"span 5 / span 5","span-6":"span 6 / span 6","span-7":"span 7 / span 7","span-8":"span 8 / span 8","span-9":"span 9 / span 9","span-10":"span 10 / span 10","span-11":"span 11 / span 11","span-12":"span 12 / span 12","span-full":"1 / -1"},gridRowEnd:{auto:"auto",1:"1",2:"2",3:"3",4:"4",5:"5",6:"6",7:"7",8:"8",9:"9",10:"10",11:"11",12:"12",13:"13"},gridRowStart:{auto:"auto",1:"1",2:"2",3:"3",4:"4",5:"5",6:"6",7:"7",8:"8",9:"9",10:"10",11:"11",12:"12",13:"13"},gridTemplateColumns:{none:"none",subgrid:"subgrid",1:"repeat(1, minmax(0, 1fr))",2:"repeat(2, minmax(0, 1fr))",3:"repeat(3, minmax(0, 1fr))",4:"repeat(4, minmax(0, 1fr))",5:"repeat(5, minmax(0, 1fr))",6:"repeat(6, minmax(0, 1fr))",7:"repeat(7, minmax(0, 1fr))",8:"repeat(8, minmax(0, 1fr))",9:"repeat(9, minmax(0, 1fr))",10:"repeat(10, minmax(0, 1fr))",11:"repeat(11, minmax(0, 1fr))",12:"repeat(12, minmax(0, 1fr))"},gridTemplateRows:{none:"none",subgrid:"subgrid",1:"repeat(1, minmax(0, 1fr))",2:"repeat(2, minmax(0, 1fr))",3:"repeat(3, minmax(0, 1fr))",4:"repeat(4, minmax(0, 1fr))",5:"repeat(5, minmax(0, 1fr))",6:"repeat(6, minmax(0, 1fr))",7:"repeat(7, minmax(0, 1fr))",8:"repeat(8, minmax(0, 1fr))",9:"repeat(9, minmax(0, 1fr))",10:"repeat(10, minmax(0, 1fr))",11:"repeat(11, minmax(0, 1fr))",12:"repeat(12, minmax(0, 1fr))"},height:({theme:r})=>({auto:"auto",...r("spacing"),"1/2":"50%","1/3":"33.333333%","2/3":"66.666667%","1/4":"25%","2/4":"50%","3/4":"75%","1/5":"20%","2/5":"40%","3/5":"60%","4/5":"80%","1/6":"16.666667%","2/6":"33.333333%","3/6":"50%","4/6":"66.666667%","5/6":"83.333333%",full:"100%",screen:"100vh",svh:"100svh",lvh:"100lvh",dvh:"100dvh",min:"min-content",max:"max-content",fit:"fit-content"}),hueRotate:{0:"0deg",15:"15deg",30:"30deg",60:"60deg",90:"90deg",180:"180deg"},inset:({theme:r})=>({auto:"auto",...r("spacing"),"1/2":"50%","1/3":"33.333333%","2/3":"66.666667%","1/4":"25%","2/4":"50%","3/4":"75%",full:"100%"}),invert:{0:"0",DEFAULT:"100%"},keyframes:{spin:{to:{transform:"rotate(360deg)"}},ping:{"75%, 100%":{transform:"scale(2)",opacity:"0"}},pulse:{"50%":{opacity:".5"}},bounce:{"0%, 100%":{transform:"translateY(-25%)",animationTimingFunction:"cubic-bezier(0.8,0,1,1)"},"50%":{transform:"none",animationTimingFunction:"cubic-bezier(0,0,0.2,1)"}}},letterSpacing:{tighter:"-0.05em",tight:"-0.025em",normal:"0em",wide:"0.025em",wider:"0.05em",widest:"0.1em"},lineHeight:{none:"1",tight:"1.25",snug:"1.375",normal:"1.5",relaxed:"1.625",loose:"2",3:".75rem",4:"1rem",5:"1.25rem",6:"1.5rem",7:"1.75rem",8:"2rem",9:"2.25rem",10:"2.5rem"},listStyleType:{none:"none",disc:"disc",decimal:"decimal"},listStyleImage:{none:"none"},margin:({theme:r})=>({auto:"auto",...r("spacing")}),lineClamp:{1:"1",2:"2",3:"3",4:"4",5:"5",6:"6"},maxHeight:({theme:r})=>({...r("spacing"),none:"none",full:"100%",screen:"100vh",svh:"100svh",lvh:"100lvh",dvh:"100dvh",min:"min-content",max:"max-content",fit:"fit-content"}),maxWidth:({theme:r,breakpoints:e})=>({...r("spacing"),none:"none",xs:"20rem",sm:"24rem",md:"28rem",lg:"32rem",xl:"36rem","2xl":"42rem","3xl":"48rem","4xl":"56rem","5xl":"64rem","6xl":"72rem","7xl":"80rem",full:"100%",min:"min-content",max:"max-content",fit:"fit-content",prose:"65ch",...e(r("screens"))}),minHeight:({theme:r})=>({...r("spacing"),full:"100%",screen:"100vh",svh:"100svh",lvh:"100lvh",dvh:"100dvh",min:"min-content",max:"max-content",fit:"fit-content"}),minWidth:({theme:r})=>({...r("spacing"),full:"100%",min:"min-content",max:"max-content",fit:"fit-content"}),objectPosition:{bottom:"bottom",center:"center",left:"left","left-bottom":"left bottom","left-top":"left top",right:"right","right-bottom":"right bottom","right-top":"right top",top:"top"},opacity:{0:"0",5:"0.05",10:"0.1",15:"0.15",20:"0.2",25:"0.25",30:"0.3",35:"0.35",40:"0.4",45:"0.45",50:"0.5",55:"0.55",60:"0.6",65:"0.65",70:"0.7",75:"0.75",80:"0.8",85:"0.85",90:"0.9",95:"0.95",100:"1"},order:{first:"-9999",last:"9999",none:"0",1:"1",2:"2",3:"3",4:"4",5:"5",6:"6",7:"7",8:"8",9:"9",10:"10",11:"11",12:"12"},outlineColor:({theme:r})=>r("colors"),outlineOffset:{0:"0px",1:"1px",2:"2px",4:"4px",8:"8px"},outlineWidth:{0:"0px",1:"1px",2:"2px",4:"4px",8:"8px"},padding:({theme:r})=>r("spacing"),placeholderColor:({theme:r})=>r("colors"),placeholderOpacity:({theme:r})=>r("opacity"),ringColor:({theme:r})=>({DEFAULT:r("colors.blue.500","#3b82f6"),...r("colors")}),ringOffsetColor:({theme:r})=>r("colors"),ringOffsetWidth:{0:"0px",1:"1px",2:"2px",4:"4px",8:"8px"},ringOpacity:({theme:r})=>({DEFAULT:"0.5",...r("opacity")}),ringWidth:{DEFAULT:"3px",0:"0px",1:"1px",2:"2px",4:"4px",8:"8px"},rotate:{0:"0deg",1:"1deg",2:"2deg",3:"3deg",6:"6deg",12:"12deg",45:"45deg",90:"90deg",180:"180deg"},saturate:{0:"0",50:".5",100:"1",150:"1.5",200:"2"},scale:{0:"0",50:".5",75:".75",90:".9",95:".95",100:"1",105:"1.05",110:"1.1",125:"1.25",150:"1.5"},screens:{sm:"640px",md:"768px",lg:"1024px",xl:"1280px","2xl":"1536px"},scrollMargin:({theme:r})=>({...r("spacing")}),scrollPadding:({theme:r})=>r("spacing"),sepia:{0:"0",DEFAULT:"100%"},skew:{0:"0deg",1:"1deg",2:"2deg",3:"3deg",6:"6deg",12:"12deg"},space:({theme:r})=>({...r("spacing")}),spacing:{px:"1px",0:"0px",.5:"0.125rem",1:"0.25rem",1.5:"0.375rem",2:"0.5rem",2.5:"0.625rem",3:"0.75rem",3.5:"0.875rem",4:"1rem",5:"1.25rem",6:"1.5rem",7:"1.75rem",8:"2rem",9:"2.25rem",10:"2.5rem",11:"2.75rem",12:"3rem",14:"3.5rem",16:"4rem",20:"5rem",24:"6rem",28:"7rem",32:"8rem",36:"9rem",40:"10rem",44:"11rem",48:"12rem",52:"13rem",56:"14rem",60:"15rem",64:"16rem",72:"18rem",80:"20rem",96:"24rem"},stroke:({theme:r})=>({none:"none",...r("colors")}),strokeWidth:{0:"0",1:"1",2:"2"},supports:{},data:{},textColor:({theme:r})=>r("colors"),textDecorationColor:({theme:r})=>r("colors"),textDecorationThickness:{auto:"auto","from-font":"from-font",0:"0px",1:"1px",2:"2px",4:"4px",8:"8px"},textIndent:({theme:r})=>({...r("spacing")}),textOpacity:({theme:r})=>r("opacity"),textUnderlineOffset:{auto:"auto",0:"0px",1:"1px",2:"2px",4:"4px",8:"8px"},transformOrigin:{center:"center",top:"top","top-right":"top right",right:"right","bottom-right":"bottom right",bottom:"bottom","bottom-left":"bottom left",left:"left","top-left":"top left"},transitionDelay:{0:"0s",75:"75ms",100:"100ms",150:"150ms",200:"200ms",300:"300ms",500:"500ms",700:"700ms",1e3:"1000ms"},transitionDuration:{DEFAULT:"150ms",0:"0s",75:"75ms",100:"100ms",150:"150ms",200:"200ms",300:"300ms",500:"500ms",700:"700ms",1e3:"1000ms"},transitionProperty:{none:"none",all:"all",DEFAULT:"color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter",colors:"color, background-color, border-color, text-decoration-color, fill, stroke",opacity:"opacity",shadow:"box-shadow",transform:"transform"},transitionTimingFunction:{DEFAULT:"cubic-bezier(0.4, 0, 0.2, 1)",linear:"linear",in:"cubic-bezier(0.4, 0, 1, 1)",out:"cubic-bezier(0, 0, 0.2, 1)","in-out":"cubic-bezier(0.4, 0, 0.2, 1)"},translate:({theme:r})=>({...r("spacing"),"1/2":"50%","1/3":"33.333333%","2/3":"66.666667%","1/4":"25%","2/4":"50%","3/4":"75%",full:"100%"}),size:({theme:r})=>({auto:"auto",...r("spacing"),"1/2":"50%","1/3":"33.333333%","2/3":"66.666667%","1/4":"25%","2/4":"50%","3/4":"75%","1/5":"20%","2/5":"40%","3/5":"60%","4/5":"80%","1/6":"16.666667%","2/6":"33.333333%","3/6":"50%","4/6":"66.666667%","5/6":"83.333333%","1/12":"8.333333%","2/12":"16.666667%","3/12":"25%","4/12":"33.333333%","5/12":"41.666667%","6/12":"50%","7/12":"58.333333%","8/12":"66.666667%","9/12":"75%","10/12":"83.333333%","11/12":"91.666667%",full:"100%",min:"min-content",max:"max-content",fit:"fit-content"}),width:({theme:r})=>({auto:"auto",...r("spacing"),"1/2":"50%","1/3":"33.333333%","2/3":"66.666667%","1/4":"25%","2/4":"50%","3/4":"75%","1/5":"20%","2/5":"40%","3/5":"60%","4/5":"80%","1/6":"16.666667%","2/6":"33.333333%","3/6":"50%","4/6":"66.666667%","5/6":"83.333333%","1/12":"8.333333%","2/12":"16.666667%","3/12":"25%","4/12":"33.333333%","5/12":"41.666667%","6/12":"50%","7/12":"58.333333%","8/12":"66.666667%","9/12":"75%","10/12":"83.333333%","11/12":"91.666667%",full:"100%",screen:"100vw",svw:"100svw",lvw:"100lvw",dvw:"100dvw",min:"min-content",max:"max-content",fit:"fit-content"}),willChange:{auto:"auto",scroll:"scroll-position",contents:"contents",transform:"transform"},zIndex:{auto:"auto",0:"0",10:"10",20:"20",30:"30",40:"40",50:"50"}},plugins:[]}});function nn(r){let e=(r?.presets??[ic.default]).slice().reverse().flatMap(n=>nn(n instanceof Function?n():n)),t={respectDefaultRingColorOpacity:{theme:{ringColor:({theme:n})=>({DEFAULT:"#3b82f67f",...n("colors")})}},disableColorOpacityUtilitiesByDefault:{corePlugins:{backgroundOpacity:!1,borderOpacity:!1,divideOpacity:!1,placeholderOpacity:!1,ringOpacity:!1,textOpacity:!1}}},i=Object.keys(t).filter(n=>we(r,n)).map(n=>t[n]);return[r,...i,...e]}var ic,nc=P(()=>{u();ic=pe(rn());ct()});var sc={};Ge(sc,{default:()=>zr});function zr(...r){let[,...e]=nn(r[0]);return na([...r,...e])}var sa=P(()=>{u();tc();nc()});var Ur={};Ge(Ur,{default:()=>me});var me,et=P(()=>{u();me={resolve:r=>r,extname:r=>"."+r.split(".").pop()}});function sn(r){return typeof r=="object"&&r!==null}function bx(r){return Object.keys(r).length===0}function ac(r){return typeof r=="string"||r instanceof String}function aa(r){return sn(r)&&r.config===void 0&&!bx(r)?null:sn(r)&&r.config!==void 0&&ac(r.config)?me.resolve(r.config):sn(r)&&r.config!==void 0&&sn(r.config)?null:ac(r)?me.resolve(r):wx()}function wx(){for(let r of yx)try{let e=me.resolve(r);return be.accessSync(e),e}catch(e){}return null}var yx,oc=P(()=>{u();ft();et();yx=["./tailwind.config.js","./tailwind.config.cjs","./tailwind.config.mjs","./tailwind.config.ts","./tailwind.config.cts","./tailwind.config.mts"]});var lc={};Ge(lc,{default:()=>oa});var oa,la=P(()=>{u();oa={parse:r=>({href:r})}});var ua=x(()=>{u()});var an=x((v3,cc)=>{u();"use strict";var uc=(Qi(),Af),fc=ua(),Jt=class extends Error{constructor(e,t,i,n,s,a){super(e);this.name="CssSyntaxError",this.reason=e,s&&(this.file=s),n&&(this.source=n),a&&(this.plugin=a),typeof t!="undefined"&&typeof i!="undefined"&&(typeof t=="number"?(this.line=t,this.column=i):(this.line=t.line,this.column=t.column,this.endLine=i.line,this.endColumn=i.column)),this.setMessage(),Error.captureStackTrace&&Error.captureStackTrace(this,Jt)}setMessage(){this.message=this.plugin?this.plugin+": ":"",this.message+=this.file?this.file:"",typeof this.line!="undefined"&&(this.message+=":"+this.line+":"+this.column),this.message+=": "+this.reason}showSourceCode(e){if(!this.source)return"";let t=this.source;e==null&&(e=uc.isColorSupported);let i=f=>f,n=f=>f,s=f=>f;if(e){let{bold:f,gray:d,red:p}=uc.createColors(!0);n=h=>f(p(h)),i=h=>d(h),fc&&(s=h=>fc(h))}let a=t.split(/\r?\n/),o=Math.max(this.line-3,0),l=Math.min(this.line+2,a.length),c=String(l).length;return a.slice(o,l).map((f,d)=>{let p=o+1+d,h=" "+(" "+p).slice(-c)+" | ";if(p===this.line){if(f.length>160){let v=20,y=Math.max(0,this.column-v),w=Math.max(this.column+v,this.endColumn+v),k=f.slice(y,w),S=i(h.replace(/\d/g," "))+f.slice(0,Math.min(this.column-1,v-1)).replace(/[^\t]/g," ");return n(">")+i(h)+s(k)+` + `+S+n("^")}let b=i(h.replace(/\d/g," "))+f.slice(0,this.column-1).replace(/[^\t]/g," ");return n(">")+i(h)+s(f)+` + `+b+n("^")}return" "+i(h)+s(f)}).join(` +`)}toString(){let e=this.showSourceCode();return e&&(e=` + +`+e+` +`),this.name+": "+this.message+e}};cc.exports=Jt;Jt.default=Jt});var fa=x((x3,dc)=>{u();"use strict";var pc={after:` +`,beforeClose:` +`,beforeComment:` +`,beforeDecl:` +`,beforeOpen:" ",beforeRule:` +`,colon:": ",commentLeft:" ",commentRight:" ",emptyBody:"",indent:" ",semicolon:!1};function vx(r){return r[0].toUpperCase()+r.slice(1)}var on=class{constructor(e){this.builder=e}atrule(e,t){let i="@"+e.name,n=e.params?this.rawValue(e,"params"):"";if(typeof e.raws.afterName!="undefined"?i+=e.raws.afterName:n&&(i+=" "),e.nodes)this.block(e,i+n);else{let s=(e.raws.between||"")+(t?";":"");this.builder(i+n+s,e)}}beforeAfter(e,t){let i;e.type==="decl"?i=this.raw(e,null,"beforeDecl"):e.type==="comment"?i=this.raw(e,null,"beforeComment"):t==="before"?i=this.raw(e,null,"beforeRule"):i=this.raw(e,null,"beforeClose");let n=e.parent,s=0;for(;n&&n.type!=="root";)s+=1,n=n.parent;if(i.includes(` +`)){let a=this.raw(e,null,"indent");if(a.length)for(let o=0;o0&&e.nodes[t].type==="comment";)t-=1;let i=this.raw(e,"semicolon");for(let n=0;n{if(n=l.raws[t],typeof n!="undefined")return!1})}return typeof n=="undefined"&&(n=pc[i]),a.rawCache[i]=n,n}rawBeforeClose(e){let t;return e.walk(i=>{if(i.nodes&&i.nodes.length>0&&typeof i.raws.after!="undefined")return t=i.raws.after,t.includes(` +`)&&(t=t.replace(/[^\n]+$/,"")),!1}),t&&(t=t.replace(/\S/g,"")),t}rawBeforeComment(e,t){let i;return e.walkComments(n=>{if(typeof n.raws.before!="undefined")return i=n.raws.before,i.includes(` +`)&&(i=i.replace(/[^\n]+$/,"")),!1}),typeof i=="undefined"?i=this.raw(t,null,"beforeDecl"):i&&(i=i.replace(/\S/g,"")),i}rawBeforeDecl(e,t){let i;return e.walkDecls(n=>{if(typeof n.raws.before!="undefined")return i=n.raws.before,i.includes(` +`)&&(i=i.replace(/[^\n]+$/,"")),!1}),typeof i=="undefined"?i=this.raw(t,null,"beforeRule"):i&&(i=i.replace(/\S/g,"")),i}rawBeforeOpen(e){let t;return e.walk(i=>{if(i.type!=="decl"&&(t=i.raws.between,typeof t!="undefined"))return!1}),t}rawBeforeRule(e){let t;return e.walk(i=>{if(i.nodes&&(i.parent!==e||e.first!==i)&&typeof i.raws.before!="undefined")return t=i.raws.before,t.includes(` +`)&&(t=t.replace(/[^\n]+$/,"")),!1}),t&&(t=t.replace(/\S/g,"")),t}rawColon(e){let t;return e.walkDecls(i=>{if(typeof i.raws.between!="undefined")return t=i.raws.between.replace(/[^\s:]/g,""),!1}),t}rawEmptyBody(e){let t;return e.walk(i=>{if(i.nodes&&i.nodes.length===0&&(t=i.raws.after,typeof t!="undefined"))return!1}),t}rawIndent(e){if(e.raws.indent)return e.raws.indent;let t;return e.walk(i=>{let n=i.parent;if(n&&n!==e&&n.parent&&n.parent===e&&typeof i.raws.before!="undefined"){let s=i.raws.before.split(` +`);return t=s[s.length-1],t=t.replace(/\S/g,""),!1}}),t}rawSemicolon(e){let t;return e.walk(i=>{if(i.nodes&&i.nodes.length&&i.last.type==="decl"&&(t=i.raws.semicolon,typeof t!="undefined"))return!1}),t}rawValue(e,t){let i=e[t],n=e.raws[t];return n&&n.value===i?n.raw:i}root(e){this.body(e),e.raws.after&&this.builder(e.raws.after)}rule(e){this.block(e,this.rawValue(e,"selector")),e.raws.ownSemicolon&&this.builder(e.raws.ownSemicolon,e,"end")}stringify(e,t){if(!this[e.type])throw new Error("Unknown AST node type "+e.type+". Maybe you need to change PostCSS stringifier.");this[e.type](e,t)}};dc.exports=on;on.default=on});var Vr=x((k3,hc)=>{u();"use strict";var xx=fa();function ca(r,e){new xx(e).stringify(r)}hc.exports=ca;ca.default=ca});var ln=x((S3,pa)=>{u();"use strict";pa.exports.isClean=Symbol("isClean");pa.exports.my=Symbol("my")});var Gr=x((A3,mc)=>{u();"use strict";var kx=an(),Sx=fa(),Ax=Vr(),{isClean:Hr,my:Cx}=ln();function da(r,e){let t=new r.constructor;for(let i in r){if(!Object.prototype.hasOwnProperty.call(r,i)||i==="proxyCache")continue;let n=r[i],s=typeof n;i==="parent"&&s==="object"?e&&(t[i]=e):i==="source"?t[i]=n:Array.isArray(n)?t[i]=n.map(a=>da(a,t)):(s==="object"&&n!==null&&(n=da(n)),t[i]=n)}return t}function Wr(r,e){if(e&&typeof e.offset!="undefined")return e.offset;let t=1,i=1,n=0;for(let s=0;se.root().toProxy():e[t]},set(e,t,i){return e[t]===i||(e[t]=i,(t==="prop"||t==="value"||t==="name"||t==="params"||t==="important"||t==="text")&&e.markDirty()),!0}}}markClean(){this[Hr]=!0}markDirty(){if(this[Hr]){this[Hr]=!1;let e=this;for(;e=e.parent;)e[Hr]=!1}}next(){if(!this.parent)return;let e=this.parent.index(this);return this.parent.nodes[e+1]}positionBy(e){let t=this.source.start;if(e.index)t=this.positionInside(e.index);else if(e.word){let n=this.source.input.css.slice(Wr(this.source.input.css,this.source.start),Wr(this.source.input.css,this.source.end)).indexOf(e.word);n!==-1&&(t=this.positionInside(n))}return t}positionInside(e){let t=this.source.start.column,i=this.source.start.line,n=Wr(this.source.input.css,this.source.start),s=n+e;for(let a=n;atypeof l=="object"&&l.toJSON?l.toJSON(null,t):l);else if(typeof o=="object"&&o.toJSON)i[a]=o.toJSON(null,t);else if(a==="source"){let l=t.get(o.input);l==null&&(l=s,t.set(o.input,s),s++),i[a]={end:o.end,inputId:l,start:o.start}}else i[a]=o}return n&&(i.inputs=[...t.keys()].map(a=>a.toJSON())),i}toProxy(){return this.proxyCache||(this.proxyCache=new Proxy(this,this.getProxyProcessor())),this.proxyCache}toString(e=Ax){e.stringify&&(e=e.stringify);let t="";return e(this,i=>{t+=i}),t}warn(e,t,i){let n={node:this};for(let s in i)n[s]=i[s];return e.warn(t,n)}get proxyOf(){return this}};mc.exports=un;un.default=un});var Qr=x((C3,gc)=>{u();"use strict";var _x=Gr(),fn=class extends _x{constructor(e){super(e);this.type="comment"}};gc.exports=fn;fn.default=fn});var Yr=x((_3,yc)=>{u();"use strict";var Ex=Gr(),cn=class extends Ex{constructor(e){e&&typeof e.value!="undefined"&&typeof e.value!="string"&&(e={...e,value:String(e.value)});super(e);this.type="decl"}get variable(){return this.prop.startsWith("--")||this.prop[0]==="$"}};yc.exports=cn;cn.default=cn});var Et=x((E3,_c)=>{u();"use strict";var bc=Qr(),wc=Yr(),Ox=Gr(),{isClean:vc,my:xc}=ln(),ha,kc,Sc,ma;function Ac(r){return r.map(e=>(e.nodes&&(e.nodes=Ac(e.nodes)),delete e.source,e))}function Cc(r){if(r[vc]=!1,r.proxyOf.nodes)for(let e of r.proxyOf.nodes)Cc(e)}var Fe=class extends Ox{append(...e){for(let t of e){let i=this.normalize(t,this.last);for(let n of i)this.proxyOf.nodes.push(n)}return this.markDirty(),this}cleanRaws(e){if(super.cleanRaws(e),this.nodes)for(let t of this.nodes)t.cleanRaws(e)}each(e){if(!this.proxyOf.nodes)return;let t=this.getIterator(),i,n;for(;this.indexes[t]e[t](...i.map(n=>typeof n=="function"?(s,a)=>n(s.toProxy(),a):n)):t==="every"||t==="some"?i=>e[t]((n,...s)=>i(n.toProxy(),...s)):t==="root"?()=>e.root().toProxy():t==="nodes"?e.nodes.map(i=>i.toProxy()):t==="first"||t==="last"?e[t].toProxy():e[t]:e[t]},set(e,t,i){return e[t]===i||(e[t]=i,(t==="name"||t==="params"||t==="selector")&&e.markDirty()),!0}}}index(e){return typeof e=="number"?e:(e.proxyOf&&(e=e.proxyOf),this.proxyOf.nodes.indexOf(e))}insertAfter(e,t){let i=this.index(e),n=this.normalize(t,this.proxyOf.nodes[i]).reverse();i=this.index(e);for(let a of n)this.proxyOf.nodes.splice(i+1,0,a);let s;for(let a in this.indexes)s=this.indexes[a],i(n[xc]||Fe.rebuild(n),n=n.proxyOf,n.parent&&n.parent.removeChild(n),n[vc]&&Cc(n),n.raws||(n.raws={}),typeof n.raws.before=="undefined"&&t&&typeof t.raws.before!="undefined"&&(n.raws.before=t.raws.before.replace(/\S/g,"")),n.parent=this.proxyOf,n))}prepend(...e){e=e.reverse();for(let t of e){let i=this.normalize(t,this.first,"prepend").reverse();for(let n of i)this.proxyOf.nodes.unshift(n);for(let n in this.indexes)this.indexes[n]=this.indexes[n]+i.length}return this.markDirty(),this}push(e){return e.parent=this,this.proxyOf.nodes.push(e),this}removeAll(){for(let e of this.proxyOf.nodes)e.parent=void 0;return this.proxyOf.nodes=[],this.markDirty(),this}removeChild(e){e=this.index(e),this.proxyOf.nodes[e].parent=void 0,this.proxyOf.nodes.splice(e,1);let t;for(let i in this.indexes)t=this.indexes[i],t>=e&&(this.indexes[i]=t-1);return this.markDirty(),this}replaceValues(e,t,i){return i||(i=t,t={}),this.walkDecls(n=>{t.props&&!t.props.includes(n.prop)||t.fast&&!n.value.includes(t.fast)||(n.value=n.value.replace(e,i))}),this.markDirty(),this}some(e){return this.nodes.some(e)}walk(e){return this.each((t,i)=>{let n;try{n=e(t,i)}catch(s){throw t.addToError(s)}return n!==!1&&t.walk&&(n=t.walk(e)),n})}walkAtRules(e,t){return t?e instanceof RegExp?this.walk((i,n)=>{if(i.type==="atrule"&&e.test(i.name))return t(i,n)}):this.walk((i,n)=>{if(i.type==="atrule"&&i.name===e)return t(i,n)}):(t=e,this.walk((i,n)=>{if(i.type==="atrule")return t(i,n)}))}walkComments(e){return this.walk((t,i)=>{if(t.type==="comment")return e(t,i)})}walkDecls(e,t){return t?e instanceof RegExp?this.walk((i,n)=>{if(i.type==="decl"&&e.test(i.prop))return t(i,n)}):this.walk((i,n)=>{if(i.type==="decl"&&i.prop===e)return t(i,n)}):(t=e,this.walk((i,n)=>{if(i.type==="decl")return t(i,n)}))}walkRules(e,t){return t?e instanceof RegExp?this.walk((i,n)=>{if(i.type==="rule"&&e.test(i.selector))return t(i,n)}):this.walk((i,n)=>{if(i.type==="rule"&&i.selector===e)return t(i,n)}):(t=e,this.walk((i,n)=>{if(i.type==="rule")return t(i,n)}))}get first(){if(!!this.proxyOf.nodes)return this.proxyOf.nodes[0]}get last(){if(!!this.proxyOf.nodes)return this.proxyOf.nodes[this.proxyOf.nodes.length-1]}};Fe.registerParse=r=>{kc=r};Fe.registerRule=r=>{ma=r};Fe.registerAtRule=r=>{ha=r};Fe.registerRoot=r=>{Sc=r};_c.exports=Fe;Fe.default=Fe;Fe.rebuild=r=>{r.type==="atrule"?Object.setPrototypeOf(r,ha.prototype):r.type==="rule"?Object.setPrototypeOf(r,ma.prototype):r.type==="decl"?Object.setPrototypeOf(r,wc.prototype):r.type==="comment"?Object.setPrototypeOf(r,bc.prototype):r.type==="root"&&Object.setPrototypeOf(r,Sc.prototype),r[xc]=!0,r.nodes&&r.nodes.forEach(e=>{Fe.rebuild(e)})}});var pn=x((O3,Oc)=>{u();"use strict";var Ec=Et(),Kr=class extends Ec{constructor(e){super(e);this.type="atrule"}append(...e){return this.proxyOf.nodes||(this.nodes=[]),super.append(...e)}prepend(...e){return this.proxyOf.nodes||(this.nodes=[]),super.prepend(...e)}};Oc.exports=Kr;Kr.default=Kr;Ec.registerAtRule(Kr)});var dn=x((T3,Pc)=>{u();"use strict";var Tx=Et(),Tc,Rc,er=class extends Tx{constructor(e){super({type:"document",...e});this.nodes||(this.nodes=[])}toResult(e={}){return new Tc(new Rc,this,e).stringify()}};er.registerLazyResult=r=>{Tc=r};er.registerProcessor=r=>{Rc=r};Pc.exports=er;er.default=er});var Dc=x((R3,Ic)=>{u();var Rx="useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict",Px=(r,e=21)=>(t=e)=>{let i="",n=t;for(;n--;)i+=r[Math.random()*r.length|0];return i},Ix=(r=21)=>{let e="",t=r;for(;t--;)e+=Rx[Math.random()*64|0];return e};Ic.exports={nanoid:Ix,customAlphabet:Px}});var qc=x(()=>{u()});var ga=x((D3,$c)=>{u();$c.exports={}});var mn=x((q3,Bc)=>{u();"use strict";var{nanoid:Dx}=Dc(),{isAbsolute:ya,resolve:ba}=(et(),Ur),{SourceMapConsumer:qx,SourceMapGenerator:$x}=qc(),{fileURLToPath:Lc,pathToFileURL:hn}=(la(),lc),Mc=an(),Lx=ga(),wa=ua(),va=Symbol("fromOffsetCache"),Mx=Boolean(qx&&$x),Nc=Boolean(ba&&ya),Xr=class{constructor(e,t={}){if(e===null||typeof e=="undefined"||typeof e=="object"&&!e.toString)throw new Error(`PostCSS received ${e} instead of CSS string`);if(this.css=e.toString(),this.css[0]==="\uFEFF"||this.css[0]==="\uFFFE"?(this.hasBOM=!0,this.css=this.css.slice(1)):this.hasBOM=!1,t.from&&(!Nc||/^\w+:\/\//.test(t.from)||ya(t.from)?this.file=t.from:this.file=ba(t.from)),Nc&&Mx){let i=new Lx(this.css,t);if(i.text){this.map=i;let n=i.consumer().file;!this.file&&n&&(this.file=this.mapResolve(n))}}this.file||(this.id=""),this.map&&(this.map.file=this.from)}error(e,t,i,n={}){let s,a,o;if(t&&typeof t=="object"){let c=t,f=i;if(typeof c.offset=="number"){let d=this.fromOffset(c.offset);t=d.line,i=d.col}else t=c.line,i=c.column;if(typeof f.offset=="number"){let d=this.fromOffset(f.offset);a=d.line,s=d.col}else a=f.line,s=f.column}else if(!i){let c=this.fromOffset(t);t=c.line,i=c.col}let l=this.origin(t,i,a,s);return l?o=new Mc(e,l.endLine===void 0?l.line:{column:l.column,line:l.line},l.endLine===void 0?l.column:{column:l.endColumn,line:l.endLine},l.source,l.file,n.plugin):o=new Mc(e,a===void 0?t:{column:i,line:t},a===void 0?i:{column:s,line:a},this.css,this.file,n.plugin),o.input={column:i,endColumn:s,endLine:a,line:t,source:this.css},this.file&&(hn&&(o.input.url=hn(this.file).toString()),o.input.file=this.file),o}fromOffset(e){let t,i;if(this[va])i=this[va];else{let s=this.css.split(` +`);i=new Array(s.length);let a=0;for(let o=0,l=s.length;o=t)n=i.length-1;else{let s=i.length-2,a;for(;n>1),e=i[a+1])n=a+1;else{n=a;break}}return{col:e-i[n]+1,line:n+1}}mapResolve(e){return/^\w+:\/\//.test(e)?e:ba(this.map.consumer().sourceRoot||this.map.root||".",e)}origin(e,t,i,n){if(!this.map)return!1;let s=this.map.consumer(),a=s.originalPositionFor({column:t,line:e});if(!a.source)return!1;let o;typeof i=="number"&&(o=s.originalPositionFor({column:n,line:i}));let l;ya(a.source)?l=hn(a.source):l=new URL(a.source,this.map.consumer().sourceRoot||hn(this.map.mapFile));let c={column:a.column,endColumn:o&&o.column,endLine:o&&o.line,line:a.line,url:l.toString()};if(l.protocol==="file:")if(Lc)c.file=Lc(l);else throw new Error("file: protocol is not available in this PostCSS build");let f=s.sourceContentFor(a.source);return f&&(c.source=f),c}toJSON(){let e={};for(let t of["hasBOM","css","file","id"])this[t]!=null&&(e[t]=this[t]);return this.map&&(e.map={...this.map},e.map.consumerCache&&(e.map.consumerCache=void 0)),e}get from(){return this.file||this.id}};Bc.exports=Xr;Xr.default=Xr;wa&&wa.registerInput&&wa.registerInput(Xr)});var tr=x(($3,Uc)=>{u();"use strict";var Fc=Et(),jc,zc,Ut=class extends Fc{constructor(e){super(e);this.type="root",this.nodes||(this.nodes=[])}normalize(e,t,i){let n=super.normalize(e);if(t){if(i==="prepend")this.nodes.length>1?t.raws.before=this.nodes[1].raws.before:delete t.raws.before;else if(this.first!==t)for(let s of n)s.raws.before=t.raws.before}return n}removeChild(e,t){let i=this.index(e);return!t&&i===0&&this.nodes.length>1&&(this.nodes[1].raws.before=this.nodes[i].raws.before),super.removeChild(e)}toResult(e={}){return new jc(new zc,this,e).stringify()}};Ut.registerLazyResult=r=>{jc=r};Ut.registerProcessor=r=>{zc=r};Uc.exports=Ut;Ut.default=Ut;Fc.registerRoot(Ut)});var xa=x((L3,Vc)=>{u();"use strict";var Zr={comma(r){return Zr.split(r,[","],!0)},space(r){let e=[" ",` +`," "];return Zr.split(r,e)},split(r,e,t){let i=[],n="",s=!1,a=0,o=!1,l="",c=!1;for(let f of r)c?c=!1:f==="\\"?c=!0:o?f===l&&(o=!1):f==='"'||f==="'"?(o=!0,l=f):f==="("?a+=1:f===")"?a>0&&(a-=1):a===0&&e.includes(f)&&(s=!0),s?(n!==""&&i.push(n.trim()),n="",s=!1):n+=f;return(t||n!=="")&&i.push(n.trim()),i}};Vc.exports=Zr;Zr.default=Zr});var gn=x((M3,Wc)=>{u();"use strict";var Hc=Et(),Nx=xa(),Jr=class extends Hc{constructor(e){super(e);this.type="rule",this.nodes||(this.nodes=[])}get selectors(){return Nx.comma(this.selector)}set selectors(e){let t=this.selector?this.selector.match(/,\s*/):null,i=t?t[0]:","+this.raw("between","beforeOpen");this.selector=e.join(i)}};Wc.exports=Jr;Jr.default=Jr;Hc.registerRule(Jr)});var Qc=x((N3,Gc)=>{u();"use strict";var Bx=pn(),Fx=Qr(),jx=Yr(),zx=mn(),Ux=ga(),Vx=tr(),Hx=gn();function ei(r,e){if(Array.isArray(r))return r.map(n=>ei(n));let{inputs:t,...i}=r;if(t){e=[];for(let n of t){let s={...n,__proto__:zx.prototype};s.map&&(s.map={...s.map,__proto__:Ux.prototype}),e.push(s)}}if(i.nodes&&(i.nodes=r.nodes.map(n=>ei(n,e))),i.source){let{inputId:n,...s}=i.source;i.source=s,n!=null&&(i.source.input=e[n])}if(i.type==="root")return new Vx(i);if(i.type==="decl")return new jx(i);if(i.type==="rule")return new Hx(i);if(i.type==="comment")return new Fx(i);if(i.type==="atrule")return new Bx(i);throw new Error("Unknown node type: "+r.type)}Gc.exports=ei;ei.default=ei});var ka=x((B3,Yc)=>{u();Yc.exports=function(r,e){return{generate:()=>{let t="";return r(e,i=>{t+=i}),[t]}}}});var ep=x((F3,Jc)=>{u();"use strict";var Sa="'".charCodeAt(0),Kc='"'.charCodeAt(0),yn="\\".charCodeAt(0),Xc="/".charCodeAt(0),bn=` +`.charCodeAt(0),ti=" ".charCodeAt(0),wn="\f".charCodeAt(0),vn=" ".charCodeAt(0),xn="\r".charCodeAt(0),Wx="[".charCodeAt(0),Gx="]".charCodeAt(0),Qx="(".charCodeAt(0),Yx=")".charCodeAt(0),Kx="{".charCodeAt(0),Xx="}".charCodeAt(0),Zx=";".charCodeAt(0),Jx="*".charCodeAt(0),e1=":".charCodeAt(0),t1="@".charCodeAt(0),kn=/[\t\n\f\r "#'()/;[\\\]{}]/g,Sn=/[\t\n\f\r !"#'():;@[\\\]{}]|\/(?=\*)/g,r1=/.[\r\n"'(/\\]/,Zc=/[\da-f]/i;Jc.exports=function(e,t={}){let i=e.css.valueOf(),n=t.ignoreErrors,s,a,o,l,c,f,d,p,h,b,v=i.length,y=0,w=[],k=[];function S(){return y}function E(R){throw e.error("Unclosed "+R,y)}function T(){return k.length===0&&y>=v}function B(R){if(k.length)return k.pop();if(y>=v)return;let F=R?R.ignoreUnclosed:!1;switch(s=i.charCodeAt(y),s){case bn:case ti:case vn:case xn:case wn:{l=y;do l+=1,s=i.charCodeAt(l);while(s===ti||s===bn||s===vn||s===xn||s===wn);f=["space",i.slice(y,l)],y=l-1;break}case Wx:case Gx:case Kx:case Xx:case e1:case Zx:case Yx:{let Y=String.fromCharCode(s);f=[Y,Y,y];break}case Qx:{if(b=w.length?w.pop()[1]:"",h=i.charCodeAt(y+1),b==="url"&&h!==Sa&&h!==Kc&&h!==ti&&h!==bn&&h!==vn&&h!==wn&&h!==xn){l=y;do{if(d=!1,l=i.indexOf(")",l+1),l===-1)if(n||F){l=y;break}else E("bracket");for(p=l;i.charCodeAt(p-1)===yn;)p-=1,d=!d}while(d);f=["brackets",i.slice(y,l+1),y,l],y=l}else l=i.indexOf(")",y+1),a=i.slice(y,l+1),l===-1||r1.test(a)?f=["(","(",y]:(f=["brackets",a,y,l],y=l);break}case Sa:case Kc:{c=s===Sa?"'":'"',l=y;do{if(d=!1,l=i.indexOf(c,l+1),l===-1)if(n||F){l=y+1;break}else E("string");for(p=l;i.charCodeAt(p-1)===yn;)p-=1,d=!d}while(d);f=["string",i.slice(y,l+1),y,l],y=l;break}case t1:{kn.lastIndex=y+1,kn.test(i),kn.lastIndex===0?l=i.length-1:l=kn.lastIndex-2,f=["at-word",i.slice(y,l+1),y,l],y=l;break}case yn:{for(l=y,o=!0;i.charCodeAt(l+1)===yn;)l+=1,o=!o;if(s=i.charCodeAt(l+1),o&&s!==Xc&&s!==ti&&s!==bn&&s!==vn&&s!==xn&&s!==wn&&(l+=1,Zc.test(i.charAt(l)))){for(;Zc.test(i.charAt(l+1));)l+=1;i.charCodeAt(l+1)===ti&&(l+=1)}f=["word",i.slice(y,l+1),y,l],y=l;break}default:{s===Xc&&i.charCodeAt(y+1)===Jx?(l=i.indexOf("*/",y+2)+1,l===0&&(n||F?l=i.length:E("comment")),f=["comment",i.slice(y,l+1),y,l],y=l):(Sn.lastIndex=y+1,Sn.test(i),Sn.lastIndex===0?l=i.length-1:l=Sn.lastIndex-2,f=["word",i.slice(y,l+1),y,l],w.push(f),y=l);break}}return y++,f}function N(R){k.push(R)}return{back:N,endOfFile:T,nextToken:B,position:S}}});var sp=x((j3,np)=>{u();"use strict";var i1=pn(),n1=Qr(),s1=Yr(),a1=tr(),tp=gn(),o1=ep(),rp={empty:!0,space:!0};function l1(r){for(let e=r.length-1;e>=0;e--){let t=r[e],i=t[3]||t[2];if(i)return i}}var ip=class{constructor(e){this.input=e,this.root=new a1,this.current=this.root,this.spaces="",this.semicolon=!1,this.createTokenizer(),this.root.source={input:e,start:{column:1,line:1,offset:0}}}atrule(e){let t=new i1;t.name=e[1].slice(1),t.name===""&&this.unnamedAtrule(t,e),this.init(t,e[2]);let i,n,s,a=!1,o=!1,l=[],c=[];for(;!this.tokenizer.endOfFile();){if(e=this.tokenizer.nextToken(),i=e[0],i==="("||i==="["?c.push(i==="("?")":"]"):i==="{"&&c.length>0?c.push("}"):i===c[c.length-1]&&c.pop(),c.length===0)if(i===";"){t.source.end=this.getPosition(e[2]),t.source.end.offset++,this.semicolon=!0;break}else if(i==="{"){o=!0;break}else if(i==="}"){if(l.length>0){for(s=l.length-1,n=l[s];n&&n[0]==="space";)n=l[--s];n&&(t.source.end=this.getPosition(n[3]||n[2]),t.source.end.offset++)}this.end(e);break}else l.push(e);else l.push(e);if(this.tokenizer.endOfFile()){a=!0;break}}t.raws.between=this.spacesAndCommentsFromEnd(l),l.length?(t.raws.afterName=this.spacesAndCommentsFromStart(l),this.raw(t,"params",l),a&&(e=l[l.length-1],t.source.end=this.getPosition(e[3]||e[2]),t.source.end.offset++,this.spaces=t.raws.between,t.raws.between="")):(t.raws.afterName="",t.params=""),o&&(t.nodes=[],this.current=t)}checkMissedSemicolon(e){let t=this.colon(e);if(t===!1)return;let i=0,n;for(let s=t-1;s>=0&&(n=e[s],!(n[0]!=="space"&&(i+=1,i===2)));s--);throw this.input.error("Missed semicolon",n[0]==="word"?n[3]+1:n[2])}colon(e){let t=0,i,n,s;for(let[a,o]of e.entries()){if(n=o,s=n[0],s==="("&&(t+=1),s===")"&&(t-=1),t===0&&s===":")if(!i)this.doubleColon(n);else{if(i[0]==="word"&&i[1]==="progid")continue;return a}i=n}return!1}comment(e){let t=new n1;this.init(t,e[2]),t.source.end=this.getPosition(e[3]||e[2]),t.source.end.offset++;let i=e[1].slice(2,-2);if(/^\s*$/.test(i))t.text="",t.raws.left=i,t.raws.right="";else{let n=i.match(/^(\s*)([^]*\S)(\s*)$/);t.text=n[2],t.raws.left=n[1],t.raws.right=n[3]}}createTokenizer(){this.tokenizer=o1(this.input)}decl(e,t){let i=new s1;this.init(i,e[0][2]);let n=e[e.length-1];for(n[0]===";"&&(this.semicolon=!0,e.pop()),i.source.end=this.getPosition(n[3]||n[2]||l1(e)),i.source.end.offset++;e[0][0]!=="word";)e.length===1&&this.unknownWord(e),i.raws.before+=e.shift()[1];for(i.source.start=this.getPosition(e[0][2]),i.prop="";e.length;){let c=e[0][0];if(c===":"||c==="space"||c==="comment")break;i.prop+=e.shift()[1]}i.raws.between="";let s;for(;e.length;)if(s=e.shift(),s[0]===":"){i.raws.between+=s[1];break}else s[0]==="word"&&/\w/.test(s[1])&&this.unknownWord([s]),i.raws.between+=s[1];(i.prop[0]==="_"||i.prop[0]==="*")&&(i.raws.before+=i.prop[0],i.prop=i.prop.slice(1));let a=[],o;for(;e.length&&(o=e[0][0],!(o!=="space"&&o!=="comment"));)a.push(e.shift());this.precheckMissedSemicolon(e);for(let c=e.length-1;c>=0;c--){if(s=e[c],s[1].toLowerCase()==="!important"){i.important=!0;let f=this.stringFrom(e,c);f=this.spacesFromEnd(e)+f,f!==" !important"&&(i.raws.important=f);break}else if(s[1].toLowerCase()==="important"){let f=e.slice(0),d="";for(let p=c;p>0;p--){let h=f[p][0];if(d.trim().startsWith("!")&&h!=="space")break;d=f.pop()[1]+d}d.trim().startsWith("!")&&(i.important=!0,i.raws.important=d,e=f)}if(s[0]!=="space"&&s[0]!=="comment")break}e.some(c=>c[0]!=="space"&&c[0]!=="comment")&&(i.raws.between+=a.map(c=>c[1]).join(""),a=[]),this.raw(i,"value",a.concat(e),t),i.value.includes(":")&&!t&&this.checkMissedSemicolon(e)}doubleColon(e){throw this.input.error("Double colon",{offset:e[2]},{offset:e[2]+e[1].length})}emptyRule(e){let t=new tp;this.init(t,e[2]),t.selector="",t.raws.between="",this.current=t}end(e){this.current.nodes&&this.current.nodes.length&&(this.current.raws.semicolon=this.semicolon),this.semicolon=!1,this.current.raws.after=(this.current.raws.after||"")+this.spaces,this.spaces="",this.current.parent?(this.current.source.end=this.getPosition(e[2]),this.current.source.end.offset++,this.current=this.current.parent):this.unexpectedClose(e)}endFile(){this.current.parent&&this.unclosedBlock(),this.current.nodes&&this.current.nodes.length&&(this.current.raws.semicolon=this.semicolon),this.current.raws.after=(this.current.raws.after||"")+this.spaces,this.root.source.end=this.getPosition(this.tokenizer.position())}freeSemicolon(e){if(this.spaces+=e[1],this.current.nodes){let t=this.current.nodes[this.current.nodes.length-1];t&&t.type==="rule"&&!t.raws.ownSemicolon&&(t.raws.ownSemicolon=this.spaces,this.spaces="")}}getPosition(e){let t=this.input.fromOffset(e);return{column:t.col,line:t.line,offset:e}}init(e,t){this.current.push(e),e.source={input:this.input,start:this.getPosition(t)},e.raws.before=this.spaces,this.spaces="",e.type!=="comment"&&(this.semicolon=!1)}other(e){let t=!1,i=null,n=!1,s=null,a=[],o=e[1].startsWith("--"),l=[],c=e;for(;c;){if(i=c[0],l.push(c),i==="("||i==="[")s||(s=c),a.push(i==="("?")":"]");else if(o&&n&&i==="{")s||(s=c),a.push("}");else if(a.length===0)if(i===";")if(n){this.decl(l,o);return}else break;else if(i==="{"){this.rule(l);return}else if(i==="}"){this.tokenizer.back(l.pop()),t=!0;break}else i===":"&&(n=!0);else i===a[a.length-1]&&(a.pop(),a.length===0&&(s=null));c=this.tokenizer.nextToken()}if(this.tokenizer.endOfFile()&&(t=!0),a.length>0&&this.unclosedBracket(s),t&&n){if(!o)for(;l.length&&(c=l[l.length-1][0],!(c!=="space"&&c!=="comment"));)this.tokenizer.back(l.pop());this.decl(l,o)}else this.unknownWord(l)}parse(){let e;for(;!this.tokenizer.endOfFile();)switch(e=this.tokenizer.nextToken(),e[0]){case"space":this.spaces+=e[1];break;case";":this.freeSemicolon(e);break;case"}":this.end(e);break;case"comment":this.comment(e);break;case"at-word":this.atrule(e);break;case"{":this.emptyRule(e);break;default:this.other(e);break}this.endFile()}precheckMissedSemicolon(){}raw(e,t,i,n){let s,a,o=i.length,l="",c=!0,f,d;for(let p=0;ph+b[1],"");e.raws[t]={raw:p,value:l}}e[t]=l}rule(e){e.pop();let t=new tp;this.init(t,e[0][2]),t.raws.between=this.spacesAndCommentsFromEnd(e),this.raw(t,"selector",e),this.current=t}spacesAndCommentsFromEnd(e){let t,i="";for(;e.length&&(t=e[e.length-1][0],!(t!=="space"&&t!=="comment"));)i=e.pop()[1]+i;return i}spacesAndCommentsFromStart(e){let t,i="";for(;e.length&&(t=e[0][0],!(t!=="space"&&t!=="comment"));)i+=e.shift()[1];return i}spacesFromEnd(e){let t,i="";for(;e.length&&(t=e[e.length-1][0],t==="space");)i=e.pop()[1]+i;return i}stringFrom(e,t){let i="";for(let n=t;n{u();"use strict";var u1=Et(),f1=mn(),c1=sp();function An(r,e){let t=new f1(r,e),i=new c1(t);try{i.parse()}catch(n){throw n}return i.root}ap.exports=An;An.default=An;u1.registerParse(An)});var Aa=x((U3,op)=>{u();"use strict";var _n=class{constructor(e,t={}){if(this.type="warning",this.text=e,t.node&&t.node.source){let i=t.node.rangeBy(t);this.line=i.start.line,this.column=i.start.column,this.endLine=i.end.line,this.endColumn=i.end.column}for(let i in t)this[i]=t[i]}toString(){return this.node?this.node.error(this.text,{index:this.index,plugin:this.plugin,word:this.word}).message:this.plugin?this.plugin+": "+this.text:this.text}};op.exports=_n;_n.default=_n});var On=x((V3,lp)=>{u();"use strict";var p1=Aa(),En=class{constructor(e,t,i){this.processor=e,this.messages=[],this.root=t,this.opts=i,this.css=void 0,this.map=void 0}toString(){return this.css}warn(e,t={}){t.plugin||this.lastPlugin&&this.lastPlugin.postcssPlugin&&(t.plugin=this.lastPlugin.postcssPlugin);let i=new p1(e,t);return this.messages.push(i),i}warnings(){return this.messages.filter(e=>e.type==="warning")}get content(){return this.css}};lp.exports=En;En.default=En});var Ca=x((H3,fp)=>{u();"use strict";var up={};fp.exports=function(e){up[e]||(up[e]=!0,typeof console!="undefined"&&console.warn&&console.warn(e))}});var Oa=x((G3,hp)=>{u();"use strict";var d1=Et(),h1=dn(),m1=ka(),g1=Cn(),cp=On(),y1=tr(),b1=Vr(),{isClean:tt,my:w1}=ln(),W3=Ca(),v1={atrule:"AtRule",comment:"Comment",decl:"Declaration",document:"Document",root:"Root",rule:"Rule"},x1={AtRule:!0,AtRuleExit:!0,Comment:!0,CommentExit:!0,Declaration:!0,DeclarationExit:!0,Document:!0,DocumentExit:!0,Once:!0,OnceExit:!0,postcssPlugin:!0,prepare:!0,Root:!0,RootExit:!0,Rule:!0,RuleExit:!0},k1={Once:!0,postcssPlugin:!0,prepare:!0},rr=0;function ri(r){return typeof r=="object"&&typeof r.then=="function"}function pp(r){let e=!1,t=v1[r.type];return r.type==="decl"?e=r.prop.toLowerCase():r.type==="atrule"&&(e=r.name.toLowerCase()),e&&r.append?[t,t+"-"+e,rr,t+"Exit",t+"Exit-"+e]:e?[t,t+"-"+e,t+"Exit",t+"Exit-"+e]:r.append?[t,rr,t+"Exit"]:[t,t+"Exit"]}function dp(r){let e;return r.type==="document"?e=["Document",rr,"DocumentExit"]:r.type==="root"?e=["Root",rr,"RootExit"]:e=pp(r),{eventIndex:0,events:e,iterator:0,node:r,visitorIndex:0,visitors:[]}}function _a(r){return r[tt]=!1,r.nodes&&r.nodes.forEach(e=>_a(e)),r}var Ea={},pt=class{constructor(e,t,i){this.stringified=!1,this.processed=!1;let n;if(typeof t=="object"&&t!==null&&(t.type==="root"||t.type==="document"))n=_a(t);else if(t instanceof pt||t instanceof cp)n=_a(t.root),t.map&&(typeof i.map=="undefined"&&(i.map={}),i.map.inline||(i.map.inline=!1),i.map.prev=t.map);else{let s=g1;i.syntax&&(s=i.syntax.parse),i.parser&&(s=i.parser),s.parse&&(s=s.parse);try{n=s(t,i)}catch(a){this.processed=!0,this.error=a}n&&!n[w1]&&d1.rebuild(n)}this.result=new cp(e,n,i),this.helpers={...Ea,postcss:Ea,result:this.result},this.plugins=this.processor.plugins.map(s=>typeof s=="object"&&s.prepare?{...s,...s.prepare(this.result)}:s)}async(){return this.error?Promise.reject(this.error):this.processed?Promise.resolve(this.result):(this.processing||(this.processing=this.runAsync()),this.processing)}catch(e){return this.async().catch(e)}finally(e){return this.async().then(e,e)}getAsyncError(){throw new Error("Use process(css).then(cb) to work with async plugins")}handleError(e,t){let i=this.result.lastPlugin;try{t&&t.addToError(e),this.error=e,e.name==="CssSyntaxError"&&!e.plugin?(e.plugin=i.postcssPlugin,e.setMessage()):i.postcssVersion}catch(n){console&&console.error&&console.error(n)}return e}prepareVisitors(){this.listeners={};let e=(t,i,n)=>{this.listeners[i]||(this.listeners[i]=[]),this.listeners[i].push([t,n])};for(let t of this.plugins)if(typeof t=="object")for(let i in t){if(!x1[i]&&/^[A-Z]/.test(i))throw new Error(`Unknown event ${i} in ${t.postcssPlugin}. Try to update PostCSS (${this.processor.version} now).`);if(!k1[i])if(typeof t[i]=="object")for(let n in t[i])n==="*"?e(t,i,t[i][n]):e(t,i+"-"+n.toLowerCase(),t[i][n]);else typeof t[i]=="function"&&e(t,i,t[i])}this.hasListener=Object.keys(this.listeners).length>0}async runAsync(){this.plugin=0;for(let e=0;e0;){let i=this.visitTick(t);if(ri(i))try{await i}catch(n){let s=t[t.length-1].node;throw this.handleError(n,s)}}}if(this.listeners.OnceExit)for(let[t,i]of this.listeners.OnceExit){this.result.lastPlugin=t;try{if(e.type==="document"){let n=e.nodes.map(s=>i(s,this.helpers));await Promise.all(n)}else await i(e,this.helpers)}catch(n){throw this.handleError(n)}}}return this.processed=!0,this.stringify()}runOnRoot(e){this.result.lastPlugin=e;try{if(typeof e=="object"&&e.Once){if(this.result.root.type==="document"){let t=this.result.root.nodes.map(i=>e.Once(i,this.helpers));return ri(t[0])?Promise.all(t):t}return e.Once(this.result.root,this.helpers)}else if(typeof e=="function")return e(this.result.root,this.result)}catch(t){throw this.handleError(t)}}stringify(){if(this.error)throw this.error;if(this.stringified)return this.result;this.stringified=!0,this.sync();let e=this.result.opts,t=b1;e.syntax&&(t=e.syntax.stringify),e.stringifier&&(t=e.stringifier),t.stringify&&(t=t.stringify);let n=new m1(t,this.result.root,this.result.opts).generate();return this.result.css=n[0],this.result.map=n[1],this.result}sync(){if(this.error)throw this.error;if(this.processed)return this.result;if(this.processed=!0,this.processing)throw this.getAsyncError();for(let e of this.plugins){let t=this.runOnRoot(e);if(ri(t))throw this.getAsyncError()}if(this.prepareVisitors(),this.hasListener){let e=this.result.root;for(;!e[tt];)e[tt]=!0,this.walkSync(e);if(this.listeners.OnceExit)if(e.type==="document")for(let t of e.nodes)this.visitSync(this.listeners.OnceExit,t);else this.visitSync(this.listeners.OnceExit,e)}return this.result}then(e,t){return this.async().then(e,t)}toString(){return this.css}visitSync(e,t){for(let[i,n]of e){this.result.lastPlugin=i;let s;try{s=n(t,this.helpers)}catch(a){throw this.handleError(a,t.proxyOf)}if(t.type!=="root"&&t.type!=="document"&&!t.parent)return!0;if(ri(s))throw this.getAsyncError()}}visitTick(e){let t=e[e.length-1],{node:i,visitors:n}=t;if(i.type!=="root"&&i.type!=="document"&&!i.parent){e.pop();return}if(n.length>0&&t.visitorIndex{n[tt]||this.walkSync(n)});else{let n=this.listeners[i];if(n&&this.visitSync(n,e.toProxy()))return}}warnings(){return this.sync().warnings()}get content(){return this.stringify().content}get css(){return this.stringify().css}get map(){return this.stringify().map}get messages(){return this.sync().messages}get opts(){return this.result.opts}get processor(){return this.result.processor}get root(){return this.sync().root}get[Symbol.toStringTag](){return"LazyResult"}};pt.registerPostcss=r=>{Ea=r};hp.exports=pt;pt.default=pt;y1.registerLazyResult(pt);h1.registerLazyResult(pt)});var gp=x((Y3,mp)=>{u();"use strict";var S1=ka(),A1=Cn(),C1=On(),_1=Vr(),Q3=Ca(),Tn=class{constructor(e,t,i){t=t.toString(),this.stringified=!1,this._processor=e,this._css=t,this._opts=i,this._map=void 0;let n,s=_1;this.result=new C1(this._processor,n,this._opts),this.result.css=t;let a=this;Object.defineProperty(this.result,"root",{get(){return a.root}});let o=new S1(s,n,this._opts,t);if(o.isMap()){let[l,c]=o.generate();l&&(this.result.css=l),c&&(this.result.map=c)}else o.clearAnnotation(),this.result.css=o.css}async(){return this.error?Promise.reject(this.error):Promise.resolve(this.result)}catch(e){return this.async().catch(e)}finally(e){return this.async().then(e,e)}sync(){if(this.error)throw this.error;return this.result}then(e,t){return this.async().then(e,t)}toString(){return this._css}warnings(){return[]}get content(){return this.result.css}get css(){return this.result.css}get map(){return this.result.map}get messages(){return[]}get opts(){return this.result.opts}get processor(){return this.result.processor}get root(){if(this._root)return this._root;let e,t=A1;try{e=t(this._css,this._opts)}catch(i){this.error=i}if(this.error)throw this.error;return this._root=e,e}get[Symbol.toStringTag](){return"NoWorkResult"}};mp.exports=Tn;Tn.default=Tn});var bp=x((K3,yp)=>{u();"use strict";var E1=dn(),O1=Oa(),T1=gp(),R1=tr(),ir=class{constructor(e=[]){this.version="8.4.49",this.plugins=this.normalize(e)}normalize(e){let t=[];for(let i of e)if(i.postcss===!0?i=i():i.postcss&&(i=i.postcss),typeof i=="object"&&Array.isArray(i.plugins))t=t.concat(i.plugins);else if(typeof i=="object"&&i.postcssPlugin)t.push(i);else if(typeof i=="function")t.push(i);else if(!(typeof i=="object"&&(i.parse||i.stringify)))throw new Error(i+" is not a PostCSS plugin");return t}process(e,t={}){return!this.plugins.length&&!t.parser&&!t.stringifier&&!t.syntax?new T1(this,e,t):new O1(this,e,t)}use(e){return this.plugins=this.plugins.concat(this.normalize([e])),this}};yp.exports=ir;ir.default=ir;R1.registerProcessor(ir);E1.registerProcessor(ir)});var $e=x((X3,Cp)=>{u();"use strict";var wp=pn(),vp=Qr(),P1=Et(),I1=an(),xp=Yr(),kp=dn(),D1=Qc(),q1=mn(),$1=Oa(),L1=xa(),M1=Gr(),N1=Cn(),Ta=bp(),B1=On(),Sp=tr(),Ap=gn(),F1=Vr(),j1=Aa();function J(...r){return r.length===1&&Array.isArray(r[0])&&(r=r[0]),new Ta(r)}J.plugin=function(e,t){let i=!1;function n(...a){console&&console.warn&&!i&&(i=!0,console.warn(e+`: postcss.plugin was deprecated. Migration guide: +https://evilmartians.com/chronicles/postcss-8-plugin-migration`),m.env.LANG&&m.env.LANG.startsWith("cn")&&console.warn(e+`: \u91CC\u9762 postcss.plugin \u88AB\u5F03\u7528. \u8FC1\u79FB\u6307\u5357: +https://www.w3ctech.com/topic/2226`));let o=t(...a);return o.postcssPlugin=e,o.postcssVersion=new Ta().version,o}let s;return Object.defineProperty(n,"postcss",{get(){return s||(s=n()),s}}),n.process=function(a,o,l){return J([n(l)]).process(a,o)},n};J.stringify=F1;J.parse=N1;J.fromJSON=D1;J.list=L1;J.comment=r=>new vp(r);J.atRule=r=>new wp(r);J.decl=r=>new xp(r);J.rule=r=>new Ap(r);J.root=r=>new Sp(r);J.document=r=>new kp(r);J.CssSyntaxError=I1;J.Declaration=xp;J.Container=P1;J.Processor=Ta;J.Document=kp;J.Comment=vp;J.Warning=j1;J.AtRule=wp;J.Result=B1;J.Input=q1;J.Rule=Ap;J.Root=Sp;J.Node=M1;$1.registerPostcss(J);Cp.exports=J;J.default=J});var re,ee,Z3,J3,eI,tI,rI,iI,nI,sI,aI,oI,lI,uI,fI,cI,pI,dI,hI,mI,gI,yI,bI,wI,vI,xI,Ot=P(()=>{u();re=pe($e()),ee=re.default,Z3=re.default.stringify,J3=re.default.fromJSON,eI=re.default.plugin,tI=re.default.parse,rI=re.default.list,iI=re.default.document,nI=re.default.comment,sI=re.default.atRule,aI=re.default.rule,oI=re.default.decl,lI=re.default.root,uI=re.default.CssSyntaxError,fI=re.default.Declaration,cI=re.default.Container,pI=re.default.Processor,dI=re.default.Document,hI=re.default.Comment,mI=re.default.Warning,gI=re.default.AtRule,yI=re.default.Result,bI=re.default.Input,wI=re.default.Rule,vI=re.default.Root,xI=re.default.Node});var Ra=x((SI,_p)=>{u();_p.exports=function(r,e,t,i,n){for(e=e.split?e.split("."):e,i=0;i{u();"use strict";Rn.__esModule=!0;Rn.default=V1;function z1(r){for(var e=r.toLowerCase(),t="",i=!1,n=0;n<6&&e[n]!==void 0;n++){var s=e.charCodeAt(n),a=s>=97&&s<=102||s>=48&&s<=57;if(i=s===32,!a)break;t+=e[n]}if(t.length!==0){var o=parseInt(t,16),l=o>=55296&&o<=57343;return l||o===0||o>1114111?["\uFFFD",t.length+(i?1:0)]:[String.fromCodePoint(o),t.length+(i?1:0)]}}var U1=/\\/;function V1(r){var e=U1.test(r);if(!e)return r;for(var t="",i=0;i{u();"use strict";In.__esModule=!0;In.default=H1;function H1(r){for(var e=arguments.length,t=new Array(e>1?e-1:0),i=1;i0;){var n=t.shift();if(!r[n])return;r=r[n]}return r}Op.exports=In.default});var Pp=x((Dn,Rp)=>{u();"use strict";Dn.__esModule=!0;Dn.default=W1;function W1(r){for(var e=arguments.length,t=new Array(e>1?e-1:0),i=1;i0;){var n=t.shift();r[n]||(r[n]={}),r=r[n]}}Rp.exports=Dn.default});var Dp=x((qn,Ip)=>{u();"use strict";qn.__esModule=!0;qn.default=G1;function G1(r){for(var e="",t=r.indexOf("/*"),i=0;t>=0;){e=e+r.slice(i,t);var n=r.indexOf("*/",t+2);if(n<0)return e;i=n+2,t=r.indexOf("/*",i)}return e=e+r.slice(i),e}Ip.exports=qn.default});var ii=x(rt=>{u();"use strict";rt.__esModule=!0;rt.unesc=rt.stripComments=rt.getProp=rt.ensureObject=void 0;var Q1=$n(Pn());rt.unesc=Q1.default;var Y1=$n(Tp());rt.getProp=Y1.default;var K1=$n(Pp());rt.ensureObject=K1.default;var X1=$n(Dp());rt.stripComments=X1.default;function $n(r){return r&&r.__esModule?r:{default:r}}});var dt=x((ni,Lp)=>{u();"use strict";ni.__esModule=!0;ni.default=void 0;var qp=ii();function $p(r,e){for(var t=0;ti||this.source.end.linen||this.source.end.line===i&&this.source.end.column{u();"use strict";ie.__esModule=!0;ie.UNIVERSAL=ie.TAG=ie.STRING=ie.SELECTOR=ie.ROOT=ie.PSEUDO=ie.NESTING=ie.ID=ie.COMMENT=ie.COMBINATOR=ie.CLASS=ie.ATTRIBUTE=void 0;var tk="tag";ie.TAG=tk;var rk="string";ie.STRING=rk;var ik="selector";ie.SELECTOR=ik;var nk="root";ie.ROOT=nk;var sk="pseudo";ie.PSEUDO=sk;var ak="nesting";ie.NESTING=ak;var ok="id";ie.ID=ok;var lk="comment";ie.COMMENT=lk;var uk="combinator";ie.COMBINATOR=uk;var fk="class";ie.CLASS=fk;var ck="attribute";ie.ATTRIBUTE=ck;var pk="universal";ie.UNIVERSAL=pk});var Ln=x((si,Fp)=>{u();"use strict";si.__esModule=!0;si.default=void 0;var dk=mk(dt()),ht=hk(Se());function Mp(r){if(typeof WeakMap!="function")return null;var e=new WeakMap,t=new WeakMap;return(Mp=function(n){return n?t:e})(r)}function hk(r,e){if(!e&&r&&r.__esModule)return r;if(r===null||typeof r!="object"&&typeof r!="function")return{default:r};var t=Mp(e);if(t&&t.has(r))return t.get(r);var i={},n=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var s in r)if(s!=="default"&&Object.prototype.hasOwnProperty.call(r,s)){var a=n?Object.getOwnPropertyDescriptor(r,s):null;a&&(a.get||a.set)?Object.defineProperty(i,s,a):i[s]=r[s]}return i.default=r,t&&t.set(r,i),i}function mk(r){return r&&r.__esModule?r:{default:r}}function gk(r,e){var t=typeof Symbol!="undefined"&&r[Symbol.iterator]||r["@@iterator"];if(t)return(t=t.call(r)).next.bind(t);if(Array.isArray(r)||(t=yk(r))||e&&r&&typeof r.length=="number"){t&&(r=t);var i=0;return function(){return i>=r.length?{done:!0}:{done:!1,value:r[i++]}}}throw new TypeError(`Invalid attempt to iterate non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function yk(r,e){if(!!r){if(typeof r=="string")return Np(r,e);var t=Object.prototype.toString.call(r).slice(8,-1);if(t==="Object"&&r.constructor&&(t=r.constructor.name),t==="Map"||t==="Set")return Array.from(r);if(t==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t))return Np(r,e)}}function Np(r,e){(e==null||e>r.length)&&(e=r.length);for(var t=0,i=new Array(e);t=n&&(this.indexes[a]=s-1);return this},t.removeAll=function(){for(var n=gk(this.nodes),s;!(s=n()).done;){var a=s.value;a.parent=void 0}return this.nodes=[],this},t.empty=function(){return this.removeAll()},t.insertAfter=function(n,s){s.parent=this;var a=this.index(n);this.nodes.splice(a+1,0,s),s.parent=this;var o;for(var l in this.indexes)o=this.indexes[l],a<=o&&(this.indexes[l]=o+1);return this},t.insertBefore=function(n,s){s.parent=this;var a=this.index(n);this.nodes.splice(a,0,s),s.parent=this;var o;for(var l in this.indexes)o=this.indexes[l],o<=a&&(this.indexes[l]=o+1);return this},t._findChildAtPosition=function(n,s){var a=void 0;return this.each(function(o){if(o.atPosition){var l=o.atPosition(n,s);if(l)return a=l,!1}else if(o.isAtPosition(n,s))return a=o,!1}),a},t.atPosition=function(n,s){if(this.isAtPosition(n,s))return this._findChildAtPosition(n,s)||this},t._inferEndPosition=function(){this.last&&this.last.source&&this.last.source.end&&(this.source=this.source||{},this.source.end=this.source.end||{},Object.assign(this.source.end,this.last.source.end))},t.each=function(n){this.lastEach||(this.lastEach=0),this.indexes||(this.indexes={}),this.lastEach++;var s=this.lastEach;if(this.indexes[s]=0,!!this.length){for(var a,o;this.indexes[s]{u();"use strict";ai.__esModule=!0;ai.default=void 0;var xk=Sk(Ln()),kk=Se();function Sk(r){return r&&r.__esModule?r:{default:r}}function jp(r,e){for(var t=0;t{u();"use strict";oi.__esModule=!0;oi.default=void 0;var Ek=Tk(Ln()),Ok=Se();function Tk(r){return r&&r.__esModule?r:{default:r}}function Rk(r,e){r.prototype=Object.create(e.prototype),r.prototype.constructor=r,qa(r,e)}function qa(r,e){return qa=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(i,n){return i.__proto__=n,i},qa(r,e)}var Pk=function(r){Rk(e,r);function e(t){var i;return i=r.call(this,t)||this,i.type=Ok.SELECTOR,i}return e}(Ek.default);oi.default=Pk;Up.exports=oi.default});var Mn=x((_I,Vp)=>{u();"use strict";var Ik={},Dk=Ik.hasOwnProperty,qk=function(e,t){if(!e)return t;var i={};for(var n in t)i[n]=Dk.call(e,n)?e[n]:t[n];return i},$k=/[ -,\.\/:-@\[-\^`\{-~]/,Lk=/[ -,\.\/:-@\[\]\^`\{-~]/,Mk=/(^|\\+)?(\\[A-F0-9]{1,6})\x20(?![a-fA-F0-9\x20])/g,La=function r(e,t){t=qk(t,r.options),t.quotes!="single"&&t.quotes!="double"&&(t.quotes="single");for(var i=t.quotes=="double"?'"':"'",n=t.isIdentifier,s=e.charAt(0),a="",o=0,l=e.length;o126){if(f>=55296&&f<=56319&&o{u();"use strict";li.__esModule=!0;li.default=void 0;var Nk=Hp(Mn()),Bk=ii(),Fk=Hp(dt()),jk=Se();function Hp(r){return r&&r.__esModule?r:{default:r}}function Wp(r,e){for(var t=0;t{u();"use strict";ui.__esModule=!0;ui.default=void 0;var Hk=Gk(dt()),Wk=Se();function Gk(r){return r&&r.__esModule?r:{default:r}}function Qk(r,e){r.prototype=Object.create(e.prototype),r.prototype.constructor=r,Ba(r,e)}function Ba(r,e){return Ba=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(i,n){return i.__proto__=n,i},Ba(r,e)}var Yk=function(r){Qk(e,r);function e(t){var i;return i=r.call(this,t)||this,i.type=Wk.COMMENT,i}return e}(Hk.default);ui.default=Yk;Qp.exports=ui.default});var za=x((fi,Yp)=>{u();"use strict";fi.__esModule=!0;fi.default=void 0;var Kk=Zk(dt()),Xk=Se();function Zk(r){return r&&r.__esModule?r:{default:r}}function Jk(r,e){r.prototype=Object.create(e.prototype),r.prototype.constructor=r,ja(r,e)}function ja(r,e){return ja=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(i,n){return i.__proto__=n,i},ja(r,e)}var eS=function(r){Jk(e,r);function e(i){var n;return n=r.call(this,i)||this,n.type=Xk.ID,n}var t=e.prototype;return t.valueToString=function(){return"#"+r.prototype.valueToString.call(this)},e}(Kk.default);fi.default=eS;Yp.exports=fi.default});var Nn=x((ci,Zp)=>{u();"use strict";ci.__esModule=!0;ci.default=void 0;var tS=Kp(Mn()),rS=ii(),iS=Kp(dt());function Kp(r){return r&&r.__esModule?r:{default:r}}function Xp(r,e){for(var t=0;t{u();"use strict";pi.__esModule=!0;pi.default=void 0;var oS=uS(Nn()),lS=Se();function uS(r){return r&&r.__esModule?r:{default:r}}function fS(r,e){r.prototype=Object.create(e.prototype),r.prototype.constructor=r,Va(r,e)}function Va(r,e){return Va=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(i,n){return i.__proto__=n,i},Va(r,e)}var cS=function(r){fS(e,r);function e(t){var i;return i=r.call(this,t)||this,i.type=lS.TAG,i}return e}(oS.default);pi.default=cS;Jp.exports=pi.default});var Ga=x((di,ed)=>{u();"use strict";di.__esModule=!0;di.default=void 0;var pS=hS(dt()),dS=Se();function hS(r){return r&&r.__esModule?r:{default:r}}function mS(r,e){r.prototype=Object.create(e.prototype),r.prototype.constructor=r,Wa(r,e)}function Wa(r,e){return Wa=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(i,n){return i.__proto__=n,i},Wa(r,e)}var gS=function(r){mS(e,r);function e(t){var i;return i=r.call(this,t)||this,i.type=dS.STRING,i}return e}(pS.default);di.default=gS;ed.exports=di.default});var Ya=x((hi,td)=>{u();"use strict";hi.__esModule=!0;hi.default=void 0;var yS=wS(Ln()),bS=Se();function wS(r){return r&&r.__esModule?r:{default:r}}function vS(r,e){r.prototype=Object.create(e.prototype),r.prototype.constructor=r,Qa(r,e)}function Qa(r,e){return Qa=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(i,n){return i.__proto__=n,i},Qa(r,e)}var xS=function(r){vS(e,r);function e(i){var n;return n=r.call(this,i)||this,n.type=bS.PSEUDO,n}var t=e.prototype;return t.toString=function(){var n=this.length?"("+this.map(String).join(",")+")":"";return[this.rawSpaceBefore,this.stringifyProperty("value"),n,this.rawSpaceAfter].join("")},e}(yS.default);hi.default=xS;td.exports=hi.default});var Bn={};Ge(Bn,{deprecate:()=>kS});function kS(r){return r}var Fn=P(()=>{u()});var id=x((EI,rd)=>{u();rd.exports=(Fn(),Bn).deprecate});var to=x(yi=>{u();"use strict";yi.__esModule=!0;yi.default=void 0;yi.unescapeValue=Ja;var mi=Xa(Mn()),SS=Xa(Pn()),AS=Xa(Nn()),CS=Se(),Ka;function Xa(r){return r&&r.__esModule?r:{default:r}}function nd(r,e){for(var t=0;t0&&!n.quoted&&o.before.length===0&&!(n.spaces.value&&n.spaces.value.after)&&(o.before=" "),sd(a,o)}))),s.push("]"),s.push(this.rawSpaceAfter),s.join("")},_S(e,[{key:"quoted",get:function(){var n=this.quoteMark;return n==="'"||n==='"'},set:function(n){RS()}},{key:"quoteMark",get:function(){return this._quoteMark},set:function(n){if(!this._constructed){this._quoteMark=n;return}this._quoteMark!==n&&(this._quoteMark=n,this._syncRawValue())}},{key:"qualifiedAttribute",get:function(){return this.qualifiedName(this.raws.attribute||this.attribute)}},{key:"insensitiveFlag",get:function(){return this.insensitive?"i":""}},{key:"value",get:function(){return this._value},set:function(n){if(this._constructed){var s=Ja(n),a=s.deprecatedUsage,o=s.unescaped,l=s.quoteMark;if(a&&TS(),o===this._value&&l===this._quoteMark)return;this._value=o,this._quoteMark=l,this._syncRawValue()}else this._value=n}},{key:"insensitive",get:function(){return this._insensitive},set:function(n){n||(this._insensitive=!1,this.raws&&(this.raws.insensitiveFlag==="I"||this.raws.insensitiveFlag==="i")&&(this.raws.insensitiveFlag=void 0)),this._insensitive=n}},{key:"attribute",get:function(){return this._attribute},set:function(n){this._handleEscapes("attribute",n),this._attribute=n}}]),e}(AS.default);yi.default=jn;jn.NO_QUOTE=null;jn.SINGLE_QUOTE="'";jn.DOUBLE_QUOTE='"';var eo=(Ka={"'":{quotes:"single",wrap:!0},'"':{quotes:"double",wrap:!0}},Ka[null]={isIdentifier:!0},Ka);function sd(r,e){return""+e.before+r+e.after}});var io=x((bi,ad)=>{u();"use strict";bi.__esModule=!0;bi.default=void 0;var DS=$S(Nn()),qS=Se();function $S(r){return r&&r.__esModule?r:{default:r}}function LS(r,e){r.prototype=Object.create(e.prototype),r.prototype.constructor=r,ro(r,e)}function ro(r,e){return ro=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(i,n){return i.__proto__=n,i},ro(r,e)}var MS=function(r){LS(e,r);function e(t){var i;return i=r.call(this,t)||this,i.type=qS.UNIVERSAL,i.value="*",i}return e}(DS.default);bi.default=MS;ad.exports=bi.default});var so=x((wi,od)=>{u();"use strict";wi.__esModule=!0;wi.default=void 0;var NS=FS(dt()),BS=Se();function FS(r){return r&&r.__esModule?r:{default:r}}function jS(r,e){r.prototype=Object.create(e.prototype),r.prototype.constructor=r,no(r,e)}function no(r,e){return no=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(i,n){return i.__proto__=n,i},no(r,e)}var zS=function(r){jS(e,r);function e(t){var i;return i=r.call(this,t)||this,i.type=BS.COMBINATOR,i}return e}(NS.default);wi.default=zS;od.exports=wi.default});var oo=x((vi,ld)=>{u();"use strict";vi.__esModule=!0;vi.default=void 0;var US=HS(dt()),VS=Se();function HS(r){return r&&r.__esModule?r:{default:r}}function WS(r,e){r.prototype=Object.create(e.prototype),r.prototype.constructor=r,ao(r,e)}function ao(r,e){return ao=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(i,n){return i.__proto__=n,i},ao(r,e)}var GS=function(r){WS(e,r);function e(t){var i;return i=r.call(this,t)||this,i.type=VS.NESTING,i.value="&",i}return e}(US.default);vi.default=GS;ld.exports=vi.default});var fd=x((zn,ud)=>{u();"use strict";zn.__esModule=!0;zn.default=QS;function QS(r){return r.sort(function(e,t){return e-t})}ud.exports=zn.default});var lo=x(M=>{u();"use strict";M.__esModule=!0;M.word=M.tilde=M.tab=M.str=M.space=M.slash=M.singleQuote=M.semicolon=M.plus=M.pipe=M.openSquare=M.openParenthesis=M.newline=M.greaterThan=M.feed=M.equals=M.doubleQuote=M.dollar=M.cr=M.comment=M.comma=M.combinator=M.colon=M.closeSquare=M.closeParenthesis=M.caret=M.bang=M.backslash=M.at=M.asterisk=M.ampersand=void 0;var YS=38;M.ampersand=YS;var KS=42;M.asterisk=KS;var XS=64;M.at=XS;var ZS=44;M.comma=ZS;var JS=58;M.colon=JS;var eA=59;M.semicolon=eA;var tA=40;M.openParenthesis=tA;var rA=41;M.closeParenthesis=rA;var iA=91;M.openSquare=iA;var nA=93;M.closeSquare=nA;var sA=36;M.dollar=sA;var aA=126;M.tilde=aA;var oA=94;M.caret=oA;var lA=43;M.plus=lA;var uA=61;M.equals=uA;var fA=124;M.pipe=fA;var cA=62;M.greaterThan=cA;var pA=32;M.space=pA;var cd=39;M.singleQuote=cd;var dA=34;M.doubleQuote=dA;var hA=47;M.slash=hA;var mA=33;M.bang=mA;var gA=92;M.backslash=gA;var yA=13;M.cr=yA;var bA=12;M.feed=bA;var wA=10;M.newline=wA;var vA=9;M.tab=vA;var xA=cd;M.str=xA;var kA=-1;M.comment=kA;var SA=-2;M.word=SA;var AA=-3;M.combinator=AA});var hd=x(xi=>{u();"use strict";xi.__esModule=!0;xi.FIELDS=void 0;xi.default=PA;var D=CA(lo()),nr,te;function pd(r){if(typeof WeakMap!="function")return null;var e=new WeakMap,t=new WeakMap;return(pd=function(n){return n?t:e})(r)}function CA(r,e){if(!e&&r&&r.__esModule)return r;if(r===null||typeof r!="object"&&typeof r!="function")return{default:r};var t=pd(e);if(t&&t.has(r))return t.get(r);var i={},n=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var s in r)if(s!=="default"&&Object.prototype.hasOwnProperty.call(r,s)){var a=n?Object.getOwnPropertyDescriptor(r,s):null;a&&(a.get||a.set)?Object.defineProperty(i,s,a):i[s]=r[s]}return i.default=r,t&&t.set(r,i),i}var _A=(nr={},nr[D.tab]=!0,nr[D.newline]=!0,nr[D.cr]=!0,nr[D.feed]=!0,nr),EA=(te={},te[D.space]=!0,te[D.tab]=!0,te[D.newline]=!0,te[D.cr]=!0,te[D.feed]=!0,te[D.ampersand]=!0,te[D.asterisk]=!0,te[D.bang]=!0,te[D.comma]=!0,te[D.colon]=!0,te[D.semicolon]=!0,te[D.openParenthesis]=!0,te[D.closeParenthesis]=!0,te[D.openSquare]=!0,te[D.closeSquare]=!0,te[D.singleQuote]=!0,te[D.doubleQuote]=!0,te[D.plus]=!0,te[D.pipe]=!0,te[D.tilde]=!0,te[D.greaterThan]=!0,te[D.equals]=!0,te[D.dollar]=!0,te[D.caret]=!0,te[D.slash]=!0,te),uo={},dd="0123456789abcdefABCDEF";for(Un=0;Un0?(k=a+v,S=w-y[v].length):(k=a,S=s),T=D.comment,a=k,p=k,d=w-S):c===D.slash?(w=o,T=c,p=a,d=o-s,l=w+1):(w=OA(t,o),T=D.word,p=a,d=w-s),l=w+1;break}e.push([T,a,o-s,p,d,o,l]),S&&(s=S,S=null),o=l}return e}});var kd=x((ki,xd)=>{u();"use strict";ki.__esModule=!0;ki.default=void 0;var IA=je(Da()),fo=je($a()),DA=je(Na()),md=je(Fa()),qA=je(za()),$A=je(Ha()),co=je(Ga()),LA=je(Ya()),gd=Vn(to()),MA=je(io()),po=je(so()),NA=je(oo()),BA=je(fd()),O=Vn(hd()),q=Vn(lo()),FA=Vn(Se()),ue=ii(),Vt,ho;function yd(r){if(typeof WeakMap!="function")return null;var e=new WeakMap,t=new WeakMap;return(yd=function(n){return n?t:e})(r)}function Vn(r,e){if(!e&&r&&r.__esModule)return r;if(r===null||typeof r!="object"&&typeof r!="function")return{default:r};var t=yd(e);if(t&&t.has(r))return t.get(r);var i={},n=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var s in r)if(s!=="default"&&Object.prototype.hasOwnProperty.call(r,s)){var a=n?Object.getOwnPropertyDescriptor(r,s):null;a&&(a.get||a.set)?Object.defineProperty(i,s,a):i[s]=r[s]}return i.default=r,t&&t.set(r,i),i}function je(r){return r&&r.__esModule?r:{default:r}}function bd(r,e){for(var t=0;t0){var a=this.current.last;if(a){var o=this.convertWhitespaceNodesToSpace(s),l=o.space,c=o.rawSpace;c!==void 0&&(a.rawSpaceAfter+=c),a.spaces.after+=l}else s.forEach(function(T){return i.newNode(T)})}return}var f=this.currToken,d=void 0;n>this.position&&(d=this.parseWhitespaceEquivalentTokens(n));var p;if(this.isNamedCombinator()?p=this.namedCombinator():this.currToken[O.FIELDS.TYPE]===q.combinator?(p=new po.default({value:this.content(),source:sr(this.currToken),sourceIndex:this.currToken[O.FIELDS.START_POS]}),this.position++):mo[this.currToken[O.FIELDS.TYPE]]||d||this.unexpected(),p){if(d){var h=this.convertWhitespaceNodesToSpace(d),b=h.space,v=h.rawSpace;p.spaces.before=b,p.rawSpaceBefore=v}}else{var y=this.convertWhitespaceNodesToSpace(d,!0),w=y.space,k=y.rawSpace;k||(k=w);var S={},E={spaces:{}};w.endsWith(" ")&&k.endsWith(" ")?(S.before=w.slice(0,w.length-1),E.spaces.before=k.slice(0,k.length-1)):w.startsWith(" ")&&k.startsWith(" ")?(S.after=w.slice(1),E.spaces.after=k.slice(1)):E.value=k,p=new po.default({value:" ",source:go(f,this.tokens[this.position-1]),sourceIndex:f[O.FIELDS.START_POS],spaces:S,raws:E})}return this.currToken&&this.currToken[O.FIELDS.TYPE]===q.space&&(p.spaces.after=this.optionalSpace(this.content()),this.position++),this.newNode(p)},e.comma=function(){if(this.position===this.tokens.length-1){this.root.trailingComma=!0,this.position++;return}this.current._inferEndPosition();var i=new fo.default({source:{start:wd(this.tokens[this.position+1])},sourceIndex:this.tokens[this.position+1][O.FIELDS.START_POS]});this.current.parent.append(i),this.current=i,this.position++},e.comment=function(){var i=this.currToken;this.newNode(new md.default({value:this.content(),source:sr(i),sourceIndex:i[O.FIELDS.START_POS]})),this.position++},e.error=function(i,n){throw this.root.error(i,n)},e.missingBackslash=function(){return this.error("Expected a backslash preceding the semicolon.",{index:this.currToken[O.FIELDS.START_POS]})},e.missingParenthesis=function(){return this.expected("opening parenthesis",this.currToken[O.FIELDS.START_POS])},e.missingSquareBracket=function(){return this.expected("opening square bracket",this.currToken[O.FIELDS.START_POS])},e.unexpected=function(){return this.error("Unexpected '"+this.content()+"'. Escaping special characters with \\ may help.",this.currToken[O.FIELDS.START_POS])},e.unexpectedPipe=function(){return this.error("Unexpected '|'.",this.currToken[O.FIELDS.START_POS])},e.namespace=function(){var i=this.prevToken&&this.content(this.prevToken)||!0;if(this.nextToken[O.FIELDS.TYPE]===q.word)return this.position++,this.word(i);if(this.nextToken[O.FIELDS.TYPE]===q.asterisk)return this.position++,this.universal(i);this.unexpectedPipe()},e.nesting=function(){if(this.nextToken){var i=this.content(this.nextToken);if(i==="|"){this.position++;return}}var n=this.currToken;this.newNode(new NA.default({value:this.content(),source:sr(n),sourceIndex:n[O.FIELDS.START_POS]})),this.position++},e.parentheses=function(){var i=this.current.last,n=1;if(this.position++,i&&i.type===FA.PSEUDO){var s=new fo.default({source:{start:wd(this.tokens[this.position])},sourceIndex:this.tokens[this.position][O.FIELDS.START_POS]}),a=this.current;for(i.append(s),this.current=s;this.position1&&i.nextToken&&i.nextToken[O.FIELDS.TYPE]===q.openParenthesis&&i.error("Misplaced parenthesis.",{index:i.nextToken[O.FIELDS.START_POS]})});else return this.expected(["pseudo-class","pseudo-element"],this.currToken[O.FIELDS.START_POS])},e.space=function(){var i=this.content();this.position===0||this.prevToken[O.FIELDS.TYPE]===q.comma||this.prevToken[O.FIELDS.TYPE]===q.openParenthesis||this.current.nodes.every(function(n){return n.type==="comment"})?(this.spaces=this.optionalSpace(i),this.position++):this.position===this.tokens.length-1||this.nextToken[O.FIELDS.TYPE]===q.comma||this.nextToken[O.FIELDS.TYPE]===q.closeParenthesis?(this.current.last.spaces.after=this.optionalSpace(i),this.position++):this.combinator()},e.string=function(){var i=this.currToken;this.newNode(new co.default({value:this.content(),source:sr(i),sourceIndex:i[O.FIELDS.START_POS]})),this.position++},e.universal=function(i){var n=this.nextToken;if(n&&this.content(n)==="|")return this.position++,this.namespace();var s=this.currToken;this.newNode(new MA.default({value:this.content(),source:sr(s),sourceIndex:s[O.FIELDS.START_POS]}),i),this.position++},e.splitWord=function(i,n){for(var s=this,a=this.nextToken,o=this.content();a&&~[q.dollar,q.caret,q.equals,q.word].indexOf(a[O.FIELDS.TYPE]);){this.position++;var l=this.content();if(o+=l,l.lastIndexOf("\\")===l.length-1){var c=this.nextToken;c&&c[O.FIELDS.TYPE]===q.space&&(o+=this.requiredSpace(this.content(c)),this.position++)}a=this.nextToken}var f=yo(o,".").filter(function(b){var v=o[b-1]==="\\",y=/^\d+\.\d+%$/.test(o);return!v&&!y}),d=yo(o,"#").filter(function(b){return o[b-1]!=="\\"}),p=yo(o,"#{");p.length&&(d=d.filter(function(b){return!~p.indexOf(b)}));var h=(0,BA.default)(UA([0].concat(f,d)));h.forEach(function(b,v){var y=h[v+1]||o.length,w=o.slice(b,y);if(v===0&&n)return n.call(s,w,h.length);var k,S=s.currToken,E=S[O.FIELDS.START_POS]+h[v],T=Ht(S[1],S[2]+b,S[3],S[2]+(y-1));if(~f.indexOf(b)){var B={value:w.slice(1),source:T,sourceIndex:E};k=new DA.default(ar(B,"value"))}else if(~d.indexOf(b)){var N={value:w.slice(1),source:T,sourceIndex:E};k=new qA.default(ar(N,"value"))}else{var R={value:w,source:T,sourceIndex:E};ar(R,"value"),k=new $A.default(R)}s.newNode(k,i),i=null}),this.position++},e.word=function(i){var n=this.nextToken;return n&&this.content(n)==="|"?(this.position++,this.namespace()):this.splitWord(i)},e.loop=function(){for(;this.position{u();"use strict";Si.__esModule=!0;Si.default=void 0;var HA=WA(kd());function WA(r){return r&&r.__esModule?r:{default:r}}var GA=function(){function r(t,i){this.func=t||function(){},this.funcRes=null,this.options=i}var e=r.prototype;return e._shouldUpdateSelector=function(i,n){n===void 0&&(n={});var s=Object.assign({},this.options,n);return s.updateSelector===!1?!1:typeof i!="string"},e._isLossy=function(i){i===void 0&&(i={});var n=Object.assign({},this.options,i);return n.lossless===!1},e._root=function(i,n){n===void 0&&(n={});var s=new HA.default(i,this._parseOptions(n));return s.root},e._parseOptions=function(i){return{lossy:this._isLossy(i)}},e._run=function(i,n){var s=this;return n===void 0&&(n={}),new Promise(function(a,o){try{var l=s._root(i,n);Promise.resolve(s.func(l)).then(function(c){var f=void 0;return s._shouldUpdateSelector(i,n)&&(f=l.toString(),i.selector=f),{transform:c,root:l,string:f}}).then(a,o)}catch(c){o(c);return}})},e._runSync=function(i,n){n===void 0&&(n={});var s=this._root(i,n),a=this.func(s);if(a&&typeof a.then=="function")throw new Error("Selector processor returned a promise to a synchronous call.");var o=void 0;return n.updateSelector&&typeof i!="string"&&(o=s.toString(),i.selector=o),{transform:a,root:s,string:o}},e.ast=function(i,n){return this._run(i,n).then(function(s){return s.root})},e.astSync=function(i,n){return this._runSync(i,n).root},e.transform=function(i,n){return this._run(i,n).then(function(s){return s.transform})},e.transformSync=function(i,n){return this._runSync(i,n).transform},e.process=function(i,n){return this._run(i,n).then(function(s){return s.string||s.root.toString()})},e.processSync=function(i,n){var s=this._runSync(i,n);return s.string||s.root.toString()},r}();Si.default=GA;Sd.exports=Si.default});var Cd=x(ne=>{u();"use strict";ne.__esModule=!0;ne.universal=ne.tag=ne.string=ne.selector=ne.root=ne.pseudo=ne.nesting=ne.id=ne.comment=ne.combinator=ne.className=ne.attribute=void 0;var QA=ze(to()),YA=ze(Na()),KA=ze(so()),XA=ze(Fa()),ZA=ze(za()),JA=ze(oo()),eC=ze(Ya()),tC=ze(Da()),rC=ze($a()),iC=ze(Ga()),nC=ze(Ha()),sC=ze(io());function ze(r){return r&&r.__esModule?r:{default:r}}var aC=function(e){return new QA.default(e)};ne.attribute=aC;var oC=function(e){return new YA.default(e)};ne.className=oC;var lC=function(e){return new KA.default(e)};ne.combinator=lC;var uC=function(e){return new XA.default(e)};ne.comment=uC;var fC=function(e){return new ZA.default(e)};ne.id=fC;var cC=function(e){return new JA.default(e)};ne.nesting=cC;var pC=function(e){return new eC.default(e)};ne.pseudo=pC;var dC=function(e){return new tC.default(e)};ne.root=dC;var hC=function(e){return new rC.default(e)};ne.selector=hC;var mC=function(e){return new iC.default(e)};ne.string=mC;var gC=function(e){return new nC.default(e)};ne.tag=gC;var yC=function(e){return new sC.default(e)};ne.universal=yC});var Td=x(Z=>{u();"use strict";Z.__esModule=!0;Z.isComment=Z.isCombinator=Z.isClassName=Z.isAttribute=void 0;Z.isContainer=TC;Z.isIdentifier=void 0;Z.isNamespace=RC;Z.isNesting=void 0;Z.isNode=bo;Z.isPseudo=void 0;Z.isPseudoClass=OC;Z.isPseudoElement=Od;Z.isUniversal=Z.isTag=Z.isString=Z.isSelector=Z.isRoot=void 0;var fe=Se(),Oe,bC=(Oe={},Oe[fe.ATTRIBUTE]=!0,Oe[fe.CLASS]=!0,Oe[fe.COMBINATOR]=!0,Oe[fe.COMMENT]=!0,Oe[fe.ID]=!0,Oe[fe.NESTING]=!0,Oe[fe.PSEUDO]=!0,Oe[fe.ROOT]=!0,Oe[fe.SELECTOR]=!0,Oe[fe.STRING]=!0,Oe[fe.TAG]=!0,Oe[fe.UNIVERSAL]=!0,Oe);function bo(r){return typeof r=="object"&&bC[r.type]}function Ue(r,e){return bo(e)&&e.type===r}var _d=Ue.bind(null,fe.ATTRIBUTE);Z.isAttribute=_d;var wC=Ue.bind(null,fe.CLASS);Z.isClassName=wC;var vC=Ue.bind(null,fe.COMBINATOR);Z.isCombinator=vC;var xC=Ue.bind(null,fe.COMMENT);Z.isComment=xC;var kC=Ue.bind(null,fe.ID);Z.isIdentifier=kC;var SC=Ue.bind(null,fe.NESTING);Z.isNesting=SC;var wo=Ue.bind(null,fe.PSEUDO);Z.isPseudo=wo;var AC=Ue.bind(null,fe.ROOT);Z.isRoot=AC;var CC=Ue.bind(null,fe.SELECTOR);Z.isSelector=CC;var _C=Ue.bind(null,fe.STRING);Z.isString=_C;var Ed=Ue.bind(null,fe.TAG);Z.isTag=Ed;var EC=Ue.bind(null,fe.UNIVERSAL);Z.isUniversal=EC;function Od(r){return wo(r)&&r.value&&(r.value.startsWith("::")||r.value.toLowerCase()===":before"||r.value.toLowerCase()===":after"||r.value.toLowerCase()===":first-letter"||r.value.toLowerCase()===":first-line")}function OC(r){return wo(r)&&!Od(r)}function TC(r){return!!(bo(r)&&r.walk)}function RC(r){return _d(r)||Ed(r)}});var Rd=x(Ke=>{u();"use strict";Ke.__esModule=!0;var vo=Se();Object.keys(vo).forEach(function(r){r==="default"||r==="__esModule"||r in Ke&&Ke[r]===vo[r]||(Ke[r]=vo[r])});var xo=Cd();Object.keys(xo).forEach(function(r){r==="default"||r==="__esModule"||r in Ke&&Ke[r]===xo[r]||(Ke[r]=xo[r])});var ko=Td();Object.keys(ko).forEach(function(r){r==="default"||r==="__esModule"||r in Ke&&Ke[r]===ko[r]||(Ke[r]=ko[r])})});var it=x((Ai,Id)=>{u();"use strict";Ai.__esModule=!0;Ai.default=void 0;var PC=qC(Ad()),IC=DC(Rd());function Pd(r){if(typeof WeakMap!="function")return null;var e=new WeakMap,t=new WeakMap;return(Pd=function(n){return n?t:e})(r)}function DC(r,e){if(!e&&r&&r.__esModule)return r;if(r===null||typeof r!="object"&&typeof r!="function")return{default:r};var t=Pd(e);if(t&&t.has(r))return t.get(r);var i={},n=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var s in r)if(s!=="default"&&Object.prototype.hasOwnProperty.call(r,s)){var a=n?Object.getOwnPropertyDescriptor(r,s):null;a&&(a.get||a.set)?Object.defineProperty(i,s,a):i[s]=r[s]}return i.default=r,t&&t.set(r,i),i}function qC(r){return r&&r.__esModule?r:{default:r}}var So=function(e){return new PC.default(e)};Object.assign(So,IC);delete So.__esModule;var $C=So;Ai.default=$C;Id.exports=Ai.default});function mt(r){return["fontSize","outline"].includes(r)?e=>(typeof e=="function"&&(e=e({})),Array.isArray(e)&&(e=e[0]),e):r==="fontFamily"?e=>{typeof e=="function"&&(e=e({}));let t=Array.isArray(e)&&ke(e[1])?e[0]:e;return Array.isArray(t)?t.join(", "):t}:["boxShadow","transitionProperty","transitionDuration","transitionDelay","transitionTimingFunction","backgroundImage","backgroundSize","backgroundColor","cursor","animation"].includes(r)?e=>(typeof e=="function"&&(e=e({})),Array.isArray(e)&&(e=e.join(", ")),e):["gridTemplateColumns","gridTemplateRows","objectPosition"].includes(r)?e=>(typeof e=="function"&&(e=e({})),typeof e=="string"&&(e=ee.list.comma(e).join(" ")),e):(e,t={})=>(typeof e=="function"&&(e=e(t)),e)}var Ci=P(()=>{u();Ot();Kt()});var Bd=x((MI,Oo)=>{u();var{AtRule:LC,Rule:Dd}=$e(),qd=it();function Ao(r,e){let t;try{qd(i=>{t=i}).processSync(r)}catch(i){throw r.includes(":")?e?e.error("Missed semicolon"):i:e?e.error(i.message):i}return t.at(0)}function $d(r,e){let t=!1;return r.each(i=>{if(i.type==="nesting"){let n=e.clone({});i.value!=="&"?i.replaceWith(Ao(i.value.replace("&",n.toString()))):i.replaceWith(n),t=!0}else"nodes"in i&&i.nodes&&$d(i,e)&&(t=!0)}),t}function Ld(r,e){let t=[];return r.selectors.forEach(i=>{let n=Ao(i,r);e.selectors.forEach(s=>{if(!s)return;let a=Ao(s,e);$d(a,n)||(a.prepend(qd.combinator({value:" "})),a.prepend(n.clone({}))),t.push(a.toString())})}),t}function Hn(r,e){let t=r.prev();for(e.after(r);t&&t.type==="comment";){let i=t.prev();e.after(t),t=i}return r}function MC(r){return function e(t,i,n,s=n){let a=[];if(i.each(o=>{o.type==="rule"&&n?s&&(o.selectors=Ld(t,o)):o.type==="atrule"&&o.nodes?r[o.name]?e(t,o,s):i[_o]!==!1&&a.push(o):a.push(o)}),n&&a.length){let o=t.clone({nodes:[]});for(let l of a)o.append(l);i.prepend(o)}}}function Co(r,e,t){let i=new Dd({nodes:[],selector:r});return i.append(e),t.after(i),i}function Md(r,e){let t={};for(let i of r)t[i]=!0;if(e)for(let i of e)t[i.replace(/^@/,"")]=!0;return t}function NC(r){r=r.trim();let e=r.match(/^\((.*)\)$/);if(!e)return{selector:r,type:"basic"};let t=e[1].match(/^(with(?:out)?):(.+)$/);if(t){let i=t[1]==="with",n=Object.fromEntries(t[2].trim().split(/\s+/).map(a=>[a,!0]));if(i&&n.all)return{type:"noop"};let s=a=>!!n[a];return n.all?s=()=>!0:i&&(s=a=>a==="all"?!1:!n[a]),{escapes:s,type:"withrules"}}return{type:"unknown"}}function BC(r){let e=[],t=r.parent;for(;t&&t instanceof LC;)e.push(t),t=t.parent;return e}function FC(r){let e=r[Nd];if(!e)r.after(r.nodes);else{let t=r.nodes,i,n=-1,s,a,o,l=BC(r);if(l.forEach((c,f)=>{if(e(c.name))i=c,n=f,a=o;else{let d=o;o=c.clone({nodes:[]}),d&&o.append(d),s=s||o}}),i?a?(s.append(t),i.after(a)):i.after(t):r.after(t),r.next()&&i){let c;l.slice(0,n+1).forEach((f,d,p)=>{let h=c;c=f.clone({nodes:[]}),h&&c.append(h);let b=[],y=(p[d-1]||r).next();for(;y;)b.push(y),y=y.next();c.append(b)}),c&&(a||t[t.length-1]).after(c)}}r.remove()}var _o=Symbol("rootRuleMergeSel"),Nd=Symbol("rootRuleEscapes");function jC(r){let{params:e}=r,{escapes:t,selector:i,type:n}=NC(e);if(n==="unknown")throw r.error(`Unknown @${r.name} parameter ${JSON.stringify(e)}`);if(n==="basic"&&i){let s=new Dd({nodes:r.nodes,selector:i});r.removeAll(),r.append(s)}r[Nd]=t,r[_o]=t?!t("all"):n==="noop"}var Eo=Symbol("hasRootRule");Oo.exports=(r={})=>{let e=Md(["media","supports","layer","container","starting-style"],r.bubble),t=MC(e),i=Md(["document","font-face","keyframes","-webkit-keyframes","-moz-keyframes"],r.unwrap),n=(r.rootRuleName||"at-root").replace(/^@/,""),s=r.preserveEmpty;return{Once(a){a.walkAtRules(n,o=>{jC(o),a[Eo]=!0})},postcssPlugin:"postcss-nested",RootExit(a){a[Eo]&&(a.walkAtRules(n,FC),a[Eo]=!1)},Rule(a){let o=!1,l=a,c=!1,f=[];a.each(d=>{d.type==="rule"?(f.length&&(l=Co(a.selector,f,l),f=[]),c=!0,o=!0,d.selectors=Ld(a,d),l=Hn(d,l)):d.type==="atrule"?(f.length&&(l=Co(a.selector,f,l),f=[]),d.name===n?(o=!0,t(a,d,!0,d[_o]),l=Hn(d,l)):e[d.name]?(c=!0,o=!0,t(a,d,!0),l=Hn(d,l)):i[d.name]?(c=!0,o=!0,t(a,d,!1),l=Hn(d,l)):c&&f.push(d)):d.type==="decl"&&c&&f.push(d)}),f.length&&(l=Co(a.selector,f,l)),o&&s!==!0&&(a.raws.semicolon=!0,a.nodes.length===0&&a.remove())}}};Oo.exports.postcss=!0});var Ud=x((NI,zd)=>{u();"use strict";var Fd=/-(\w|$)/g,jd=(r,e)=>e.toUpperCase(),zC=r=>(r=r.toLowerCase(),r==="float"?"cssFloat":r.startsWith("-ms-")?r.substr(1).replace(Fd,jd):r.replace(Fd,jd));zd.exports=zC});var Po=x((BI,Vd)=>{u();var UC=Ud(),VC={boxFlex:!0,boxFlexGroup:!0,columnCount:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,strokeDashoffset:!0,strokeOpacity:!0,strokeWidth:!0};function To(r){return typeof r.nodes=="undefined"?!0:Ro(r)}function Ro(r){let e,t={};return r.each(i=>{if(i.type==="atrule")e="@"+i.name,i.params&&(e+=" "+i.params),typeof t[e]=="undefined"?t[e]=To(i):Array.isArray(t[e])?t[e].push(To(i)):t[e]=[t[e],To(i)];else if(i.type==="rule"){let n=Ro(i);if(t[i.selector])for(let s in n)t[i.selector][s]=n[s];else t[i.selector]=n}else if(i.type==="decl"){i.prop[0]==="-"&&i.prop[1]==="-"||i.parent&&i.parent.selector===":export"?e=i.prop:e=UC(i.prop);let n=i.value;!isNaN(i.value)&&VC[e]&&(n=parseFloat(i.value)),i.important&&(n+=" !important"),typeof t[e]=="undefined"?t[e]=n:Array.isArray(t[e])?t[e].push(n):t[e]=[t[e],n]}}),t}Vd.exports=Ro});var Wn=x((FI,Qd)=>{u();var _i=$e(),Hd=/\s*!important\s*$/i,HC={"box-flex":!0,"box-flex-group":!0,"column-count":!0,flex:!0,"flex-grow":!0,"flex-positive":!0,"flex-shrink":!0,"flex-negative":!0,"font-weight":!0,"line-clamp":!0,"line-height":!0,opacity:!0,order:!0,orphans:!0,"tab-size":!0,widows:!0,"z-index":!0,zoom:!0,"fill-opacity":!0,"stroke-dashoffset":!0,"stroke-opacity":!0,"stroke-width":!0};function WC(r){return r.replace(/([A-Z])/g,"-$1").replace(/^ms-/,"-ms-").toLowerCase()}function Wd(r,e,t){t===!1||t===null||(e.startsWith("--")||(e=WC(e)),typeof t=="number"&&(t===0||HC[e]?t=t.toString():t+="px"),e==="css-float"&&(e="float"),Hd.test(t)?(t=t.replace(Hd,""),r.push(_i.decl({prop:e,value:t,important:!0}))):r.push(_i.decl({prop:e,value:t})))}function Gd(r,e,t){let i=_i.atRule({name:e[1],params:e[3]||""});typeof t=="object"&&(i.nodes=[],Io(t,i)),r.push(i)}function Io(r,e){let t,i,n;for(t in r)if(i=r[t],!(i===null||typeof i=="undefined"))if(t[0]==="@"){let s=t.match(/@(\S+)(\s+([\W\w]*)\s*)?/);if(Array.isArray(i))for(let a of i)Gd(e,s,a);else Gd(e,s,i)}else if(Array.isArray(i))for(let s of i)Wd(e,t,s);else typeof i=="object"?(n=_i.rule({selector:t}),Io(i,n),e.push(n)):Wd(e,t,i)}Qd.exports=function(r){let e=_i.root();return Io(r,e),e}});var Do=x((jI,Yd)=>{u();var GC=Po();Yd.exports=function(e){return console&&console.warn&&e.warnings().forEach(t=>{let i=t.plugin||"PostCSS";console.warn(i+": "+t.text)}),GC(e.root)}});var Xd=x((zI,Kd)=>{u();var QC=$e(),YC=Do(),KC=Wn();Kd.exports=function(e){let t=QC(e);return async i=>{let n=await t.process(i,{parser:KC,from:void 0});return YC(n)}}});var Jd=x((UI,Zd)=>{u();var XC=$e(),ZC=Do(),JC=Wn();Zd.exports=function(r){let e=XC(r);return t=>{let i=e.process(t,{parser:JC,from:void 0});return ZC(i)}}});var th=x((VI,eh)=>{u();var e_=Po(),t_=Wn(),r_=Xd(),i_=Jd();eh.exports={objectify:e_,parse:t_,async:r_,sync:i_}});var or,rh,HI,WI,GI,QI,ih=P(()=>{u();or=pe(th()),rh=or.default,HI=or.default.objectify,WI=or.default.parse,GI=or.default.async,QI=or.default.sync});function lr(r){return Array.isArray(r)?r.flatMap(e=>ee([(0,nh.default)({bubble:["screen"]})]).process(e,{parser:rh}).root.nodes):lr([r])}var nh,qo=P(()=>{u();Ot();nh=pe(Bd());ih()});function ur(r,e,t=!1){if(r==="")return e;let i=typeof e=="string"?(0,sh.default)().astSync(e):e;return i.walkClasses(n=>{let s=n.value,a=t&&s.startsWith("-");n.value=a?`-${r}${s.slice(1)}`:`${r}${s}`}),typeof e=="string"?i.toString():i}var sh,Gn=P(()=>{u();sh=pe(it())});function Te(r){let e=ah.default.className();return e.value=r,jt(e?.raws?.value??e.value)}var ah,fr=P(()=>{u();ah=pe(it());Zi()});function $o(r){return jt(`.${Te(r)}`)}function Qn(r,e){return $o(Ei(r,e))}function Ei(r,e){return e==="DEFAULT"?r:e==="-"||e==="-DEFAULT"?`-${r}`:e.startsWith("-")?`-${r}${e}`:e.startsWith("/")?`${r}${e}`:`${r}-${e}`}var Lo=P(()=>{u();fr();Zi()});function L(r,e=[[r,[r]]],{filterDefault:t=!1,...i}={}){let n=mt(r);return function({matchUtilities:s,theme:a}){for(let o of e){let l=Array.isArray(o[0])?o:[o];s(l.reduce((c,[f,d])=>Object.assign(c,{[f]:p=>d.reduce((h,b)=>Array.isArray(b)?Object.assign(h,{[b[0]]:b[1]}):Object.assign(h,{[b]:n(p)}),{})}),{}),{...i,values:t?Object.fromEntries(Object.entries(a(r)??{}).filter(([c])=>c!=="DEFAULT")):a(r)})}}}var oh=P(()=>{u();Ci()});function Tt(r){return r=Array.isArray(r)?r:[r],r.map(e=>{let t=e.values.map(i=>i.raw!==void 0?i.raw:[i.min&&`(min-width: ${i.min})`,i.max&&`(max-width: ${i.max})`].filter(Boolean).join(" and "));return e.not?`not all and ${t}`:t}).join(", ")}var Yn=P(()=>{u()});function Mo(r){return r.split(f_).map(t=>{let i=t.trim(),n={value:i},s=i.split(c_),a=new Set;for(let o of s)!a.has("DIRECTIONS")&&n_.has(o)?(n.direction=o,a.add("DIRECTIONS")):!a.has("PLAY_STATES")&&s_.has(o)?(n.playState=o,a.add("PLAY_STATES")):!a.has("FILL_MODES")&&a_.has(o)?(n.fillMode=o,a.add("FILL_MODES")):!a.has("ITERATION_COUNTS")&&(o_.has(o)||p_.test(o))?(n.iterationCount=o,a.add("ITERATION_COUNTS")):!a.has("TIMING_FUNCTION")&&l_.has(o)||!a.has("TIMING_FUNCTION")&&u_.some(l=>o.startsWith(`${l}(`))?(n.timingFunction=o,a.add("TIMING_FUNCTION")):!a.has("DURATION")&&lh.test(o)?(n.duration=o,a.add("DURATION")):!a.has("DELAY")&&lh.test(o)?(n.delay=o,a.add("DELAY")):a.has("NAME")?(n.unknown||(n.unknown=[]),n.unknown.push(o)):(n.name=o,a.add("NAME"));return n})}var n_,s_,a_,o_,l_,u_,f_,c_,lh,p_,uh=P(()=>{u();n_=new Set(["normal","reverse","alternate","alternate-reverse"]),s_=new Set(["running","paused"]),a_=new Set(["none","forwards","backwards","both"]),o_=new Set(["infinite"]),l_=new Set(["linear","ease","ease-in","ease-out","ease-in-out","step-start","step-end"]),u_=["cubic-bezier","steps"],f_=/\,(?![^(]*\))/g,c_=/\ +(?![^(]*\))/g,lh=/^(-?[\d.]+m?s)$/,p_=/^(\d+)$/});var fh,xe,ch=P(()=>{u();fh=r=>Object.assign({},...Object.entries(r??{}).flatMap(([e,t])=>typeof t=="object"?Object.entries(fh(t)).map(([i,n])=>({[e+(i==="DEFAULT"?"":`-${i}`)]:n})):[{[`${e}`]:t}])),xe=fh});var dh,ph=P(()=>{dh="3.4.17"});function Rt(r,e=!0){return Array.isArray(r)?r.map(t=>{if(e&&Array.isArray(t))throw new Error("The tuple syntax is not supported for `screens`.");if(typeof t=="string")return{name:t.toString(),not:!1,values:[{min:t,max:void 0}]};let[i,n]=t;return i=i.toString(),typeof n=="string"?{name:i,not:!1,values:[{min:n,max:void 0}]}:Array.isArray(n)?{name:i,not:!1,values:n.map(s=>mh(s))}:{name:i,not:!1,values:[mh(n)]}}):Rt(Object.entries(r??{}),!1)}function Kn(r){return r.values.length!==1?{result:!1,reason:"multiple-values"}:r.values[0].raw!==void 0?{result:!1,reason:"raw-values"}:r.values[0].min!==void 0&&r.values[0].max!==void 0?{result:!1,reason:"min-and-max"}:{result:!0,reason:null}}function hh(r,e,t){let i=Xn(e,r),n=Xn(t,r),s=Kn(i),a=Kn(n);if(s.reason==="multiple-values"||a.reason==="multiple-values")throw new Error("Attempted to sort a screen with multiple values. This should never happen. Please open a bug report.");if(s.reason==="raw-values"||a.reason==="raw-values")throw new Error("Attempted to sort a screen with raw values. This should never happen. Please open a bug report.");if(s.reason==="min-and-max"||a.reason==="min-and-max")throw new Error("Attempted to sort a screen with both min and max values. This should never happen. Please open a bug report.");let{min:o,max:l}=i.values[0],{min:c,max:f}=n.values[0];e.not&&([o,l]=[l,o]),t.not&&([c,f]=[f,c]),o=o===void 0?o:parseFloat(o),l=l===void 0?l:parseFloat(l),c=c===void 0?c:parseFloat(c),f=f===void 0?f:parseFloat(f);let[d,p]=r==="min"?[o,c]:[f,l];return d-p}function Xn(r,e){return typeof r=="object"?r:{name:"arbitrary-screen",values:[{[e]:r}]}}function mh({"min-width":r,min:e=r,max:t,raw:i}={}){return{min:e,max:t,raw:i}}var Zn=P(()=>{u()});function Jn(r,e){r.walkDecls(t=>{if(e.includes(t.prop)){t.remove();return}for(let i of e)t.value.includes(`/ var(${i})`)?t.value=t.value.replace(`/ var(${i})`,""):t.value.includes(`/ var(${i}, 1)`)&&(t.value=t.value.replace(`/ var(${i}, 1)`,""))})}var gh=P(()=>{u()});var se,Xe,nt,ge,yh,bh=P(()=>{u();ft();et();Ot();oh();Yn();fr();uh();ch();Lr();ra();Kt();Ci();ph();Be();Zn();Ys();gh();ct();Br();Oi();se={childVariant:({addVariant:r})=>{r("*","& > *")},pseudoElementVariants:({addVariant:r})=>{r("first-letter","&::first-letter"),r("first-line","&::first-line"),r("marker",[({container:e})=>(Jn(e,["--tw-text-opacity"]),"& *::marker"),({container:e})=>(Jn(e,["--tw-text-opacity"]),"&::marker")]),r("selection",["& *::selection","&::selection"]),r("file","&::file-selector-button"),r("placeholder","&::placeholder"),r("backdrop","&::backdrop"),r("before",({container:e})=>(e.walkRules(t=>{let i=!1;t.walkDecls("content",()=>{i=!0}),i||t.prepend(ee.decl({prop:"content",value:"var(--tw-content)"}))}),"&::before")),r("after",({container:e})=>(e.walkRules(t=>{let i=!1;t.walkDecls("content",()=>{i=!0}),i||t.prepend(ee.decl({prop:"content",value:"var(--tw-content)"}))}),"&::after"))},pseudoClassVariants:({addVariant:r,matchVariant:e,config:t,prefix:i})=>{let n=[["first","&:first-child"],["last","&:last-child"],["only","&:only-child"],["odd","&:nth-child(odd)"],["even","&:nth-child(even)"],"first-of-type","last-of-type","only-of-type",["visited",({container:a})=>(Jn(a,["--tw-text-opacity","--tw-border-opacity","--tw-bg-opacity"]),"&:visited")],"target",["open","&[open]"],"default","checked","indeterminate","placeholder-shown","autofill","optional","required","valid","invalid","in-range","out-of-range","read-only","empty","focus-within",["hover",we(t(),"hoverOnlyWhenSupported")?"@media (hover: hover) and (pointer: fine) { &:hover }":"&:hover"],"focus","focus-visible","active","enabled","disabled"].map(a=>Array.isArray(a)?a:[a,`&:${a}`]);for(let[a,o]of n)r(a,l=>typeof o=="function"?o(l):o);let s={group:(a,{modifier:o})=>o?[`:merge(${i(".group")}\\/${Te(o)})`," &"]:[`:merge(${i(".group")})`," &"],peer:(a,{modifier:o})=>o?[`:merge(${i(".peer")}\\/${Te(o)})`," ~ &"]:[`:merge(${i(".peer")})`," ~ &"]};for(let[a,o]of Object.entries(s))e(a,(l="",c)=>{let f=K(typeof l=="function"?l(c):l);f.includes("&")||(f="&"+f);let[d,p]=o("",c),h=null,b=null,v=0;for(let y=0;y{r("ltr",'&:where([dir="ltr"], [dir="ltr"] *)'),r("rtl",'&:where([dir="rtl"], [dir="rtl"] *)')},reducedMotionVariants:({addVariant:r})=>{r("motion-safe","@media (prefers-reduced-motion: no-preference)"),r("motion-reduce","@media (prefers-reduced-motion: reduce)")},darkVariants:({config:r,addVariant:e})=>{let[t,i=".dark"]=[].concat(r("darkMode","media"));if(t===!1&&(t="media",G.warn("darkmode-false",["The `darkMode` option in your Tailwind CSS configuration is set to `false`, which now behaves the same as `media`.","Change `darkMode` to `media` or remove it entirely.","https://tailwindcss.com/docs/upgrade-guide#remove-dark-mode-configuration"])),t==="variant"){let n;if(Array.isArray(i)||typeof i=="function"?n=i:typeof i=="string"&&(n=[i]),Array.isArray(n))for(let s of n)s===".dark"?(t=!1,G.warn("darkmode-variant-without-selector",["When using `variant` for `darkMode`, you must provide a selector.",'Example: `darkMode: ["variant", ".your-selector &"]`'])):s.includes("&")||(t=!1,G.warn("darkmode-variant-without-ampersand",["When using `variant` for `darkMode`, your selector must contain `&`.",'Example `darkMode: ["variant", ".your-selector &"]`']));i=n}t==="selector"?e("dark",`&:where(${i}, ${i} *)`):t==="media"?e("dark","@media (prefers-color-scheme: dark)"):t==="variant"?e("dark",i):t==="class"&&e("dark",`&:is(${i} *)`)},printVariant:({addVariant:r})=>{r("print","@media print")},screenVariants:({theme:r,addVariant:e,matchVariant:t})=>{let i=r("screens")??{},n=Object.values(i).every(w=>typeof w=="string"),s=Rt(r("screens")),a=new Set([]);function o(w){return w.match(/(\D+)$/)?.[1]??"(none)"}function l(w){w!==void 0&&a.add(o(w))}function c(w){return l(w),a.size===1}for(let w of s)for(let k of w.values)l(k.min),l(k.max);let f=a.size<=1;function d(w){return Object.fromEntries(s.filter(k=>Kn(k).result).map(k=>{let{min:S,max:E}=k.values[0];if(w==="min"&&S!==void 0)return k;if(w==="min"&&E!==void 0)return{...k,not:!k.not};if(w==="max"&&E!==void 0)return k;if(w==="max"&&S!==void 0)return{...k,not:!k.not}}).map(k=>[k.name,k]))}function p(w){return(k,S)=>hh(w,k.value,S.value)}let h=p("max"),b=p("min");function v(w){return k=>{if(n)if(f){if(typeof k=="string"&&!c(k))return G.warn("minmax-have-mixed-units",["The `min-*` and `max-*` variants are not supported with a `screens` configuration containing mixed units."]),[]}else return G.warn("mixed-screen-units",["The `min-*` and `max-*` variants are not supported with a `screens` configuration containing mixed units."]),[];else return G.warn("complex-screen-config",["The `min-*` and `max-*` variants are not supported with a `screens` configuration containing objects."]),[];return[`@media ${Tt(Xn(k,w))}`]}}t("max",v("max"),{sort:h,values:n?d("max"):{}});let y="min-screens";for(let w of s)e(w.name,`@media ${Tt(w)}`,{id:y,sort:n&&f?b:void 0,value:w});t("min",v("min"),{id:y,sort:b})},supportsVariants:({matchVariant:r,theme:e})=>{r("supports",(t="")=>{let i=K(t),n=/^\w*\s*\(/.test(i);return i=n?i.replace(/\b(and|or|not)\b/g," $1 "):i,n?`@supports ${i}`:(i.includes(":")||(i=`${i}: var(--tw)`),i.startsWith("(")&&i.endsWith(")")||(i=`(${i})`),`@supports ${i}`)},{values:e("supports")??{}})},hasVariants:({matchVariant:r,prefix:e})=>{r("has",t=>`&:has(${K(t)})`,{values:{},[Pt]:{respectPrefix:!1}}),r("group-has",(t,{modifier:i})=>i?`:merge(${e(".group")}\\/${i}):has(${K(t)}) &`:`:merge(${e(".group")}):has(${K(t)}) &`,{values:{},[Pt]:{respectPrefix:!1}}),r("peer-has",(t,{modifier:i})=>i?`:merge(${e(".peer")}\\/${i}):has(${K(t)}) ~ &`:`:merge(${e(".peer")}):has(${K(t)}) ~ &`,{values:{},[Pt]:{respectPrefix:!1}})},ariaVariants:({matchVariant:r,theme:e})=>{r("aria",t=>`&[aria-${Ye(K(t))}]`,{values:e("aria")??{}}),r("group-aria",(t,{modifier:i})=>i?`:merge(.group\\/${i})[aria-${Ye(K(t))}] &`:`:merge(.group)[aria-${Ye(K(t))}] &`,{values:e("aria")??{}}),r("peer-aria",(t,{modifier:i})=>i?`:merge(.peer\\/${i})[aria-${Ye(K(t))}] ~ &`:`:merge(.peer)[aria-${Ye(K(t))}] ~ &`,{values:e("aria")??{}})},dataVariants:({matchVariant:r,theme:e})=>{r("data",t=>`&[data-${Ye(K(t))}]`,{values:e("data")??{}}),r("group-data",(t,{modifier:i})=>i?`:merge(.group\\/${i})[data-${Ye(K(t))}] &`:`:merge(.group)[data-${Ye(K(t))}] &`,{values:e("data")??{}}),r("peer-data",(t,{modifier:i})=>i?`:merge(.peer\\/${i})[data-${Ye(K(t))}] ~ &`:`:merge(.peer)[data-${Ye(K(t))}] ~ &`,{values:e("data")??{}})},orientationVariants:({addVariant:r})=>{r("portrait","@media (orientation: portrait)"),r("landscape","@media (orientation: landscape)")},prefersContrastVariants:({addVariant:r})=>{r("contrast-more","@media (prefers-contrast: more)"),r("contrast-less","@media (prefers-contrast: less)")},forcedColorsVariants:({addVariant:r})=>{r("forced-colors","@media (forced-colors: active)")}},Xe=["translate(var(--tw-translate-x), var(--tw-translate-y))","rotate(var(--tw-rotate))","skewX(var(--tw-skew-x))","skewY(var(--tw-skew-y))","scaleX(var(--tw-scale-x))","scaleY(var(--tw-scale-y))"].join(" "),nt=["var(--tw-blur)","var(--tw-brightness)","var(--tw-contrast)","var(--tw-grayscale)","var(--tw-hue-rotate)","var(--tw-invert)","var(--tw-saturate)","var(--tw-sepia)","var(--tw-drop-shadow)"].join(" "),ge=["var(--tw-backdrop-blur)","var(--tw-backdrop-brightness)","var(--tw-backdrop-contrast)","var(--tw-backdrop-grayscale)","var(--tw-backdrop-hue-rotate)","var(--tw-backdrop-invert)","var(--tw-backdrop-opacity)","var(--tw-backdrop-saturate)","var(--tw-backdrop-sepia)"].join(" "),yh={preflight:({addBase:r})=>{let e=ee.parse(`*,::after,::before{box-sizing:border-box;border-width:0;border-style:solid;border-color:theme('borderColor.DEFAULT', currentColor)}::after,::before{--tw-content:''}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;font-family:theme('fontFamily.sans', ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:theme('fontFamily.sans[1].fontFeatureSettings', normal);font-variation-settings:theme('fontFamily.sans[1].fontVariationSettings', normal);-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:theme('fontFamily.mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:theme('fontFamily.mono[1].fontFeatureSettings', normal);font-variation-settings:theme('fontFamily.mono[1].fontVariationSettings', normal);font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::placeholder,textarea::placeholder{opacity:1;color:theme('colors.gray.4', #9ca3af)}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}`);r([ee.comment({text:`! tailwindcss v${dh} | MIT License | https://tailwindcss.com`}),...e.nodes])},container:(()=>{function r(t=[]){return t.flatMap(i=>i.values.map(n=>n.min)).filter(i=>i!==void 0)}function e(t,i,n){if(typeof n=="undefined")return[];if(!(typeof n=="object"&&n!==null))return[{screen:"DEFAULT",minWidth:0,padding:n}];let s=[];n.DEFAULT&&s.push({screen:"DEFAULT",minWidth:0,padding:n.DEFAULT});for(let a of t)for(let o of i)for(let{min:l}of o.values)l===a&&s.push({minWidth:a,padding:n[o.name]});return s}return function({addComponents:t,theme:i}){let n=Rt(i("container.screens",i("screens"))),s=r(n),a=e(s,n,i("container.padding")),o=c=>{let f=a.find(d=>d.minWidth===c);return f?{paddingRight:f.padding,paddingLeft:f.padding}:{}},l=Array.from(new Set(s.slice().sort((c,f)=>parseInt(c)-parseInt(f)))).map(c=>({[`@media (min-width: ${c})`]:{".container":{"max-width":c,...o(c)}}}));t([{".container":Object.assign({width:"100%"},i("container.center",!1)?{marginRight:"auto",marginLeft:"auto"}:{},o(0))},...l])}})(),accessibility:({addUtilities:r})=>{r({".sr-only":{position:"absolute",width:"1px",height:"1px",padding:"0",margin:"-1px",overflow:"hidden",clip:"rect(0, 0, 0, 0)",whiteSpace:"nowrap",borderWidth:"0"},".not-sr-only":{position:"static",width:"auto",height:"auto",padding:"0",margin:"0",overflow:"visible",clip:"auto",whiteSpace:"normal"}})},pointerEvents:({addUtilities:r})=>{r({".pointer-events-none":{"pointer-events":"none"},".pointer-events-auto":{"pointer-events":"auto"}})},visibility:({addUtilities:r})=>{r({".visible":{visibility:"visible"},".invisible":{visibility:"hidden"},".collapse":{visibility:"collapse"}})},position:({addUtilities:r})=>{r({".static":{position:"static"},".fixed":{position:"fixed"},".absolute":{position:"absolute"},".relative":{position:"relative"},".sticky":{position:"sticky"}})},inset:L("inset",[["inset",["inset"]],[["inset-x",["left","right"]],["inset-y",["top","bottom"]]],[["start",["inset-inline-start"]],["end",["inset-inline-end"]],["top",["top"]],["right",["right"]],["bottom",["bottom"]],["left",["left"]]]],{supportsNegativeValues:!0}),isolation:({addUtilities:r})=>{r({".isolate":{isolation:"isolate"},".isolation-auto":{isolation:"auto"}})},zIndex:L("zIndex",[["z",["zIndex"]]],{supportsNegativeValues:!0}),order:L("order",void 0,{supportsNegativeValues:!0}),gridColumn:L("gridColumn",[["col",["gridColumn"]]]),gridColumnStart:L("gridColumnStart",[["col-start",["gridColumnStart"]]],{supportsNegativeValues:!0}),gridColumnEnd:L("gridColumnEnd",[["col-end",["gridColumnEnd"]]],{supportsNegativeValues:!0}),gridRow:L("gridRow",[["row",["gridRow"]]]),gridRowStart:L("gridRowStart",[["row-start",["gridRowStart"]]],{supportsNegativeValues:!0}),gridRowEnd:L("gridRowEnd",[["row-end",["gridRowEnd"]]],{supportsNegativeValues:!0}),float:({addUtilities:r})=>{r({".float-start":{float:"inline-start"},".float-end":{float:"inline-end"},".float-right":{float:"right"},".float-left":{float:"left"},".float-none":{float:"none"}})},clear:({addUtilities:r})=>{r({".clear-start":{clear:"inline-start"},".clear-end":{clear:"inline-end"},".clear-left":{clear:"left"},".clear-right":{clear:"right"},".clear-both":{clear:"both"},".clear-none":{clear:"none"}})},margin:L("margin",[["m",["margin"]],[["mx",["margin-left","margin-right"]],["my",["margin-top","margin-bottom"]]],[["ms",["margin-inline-start"]],["me",["margin-inline-end"]],["mt",["margin-top"]],["mr",["margin-right"]],["mb",["margin-bottom"]],["ml",["margin-left"]]]],{supportsNegativeValues:!0}),boxSizing:({addUtilities:r})=>{r({".box-border":{"box-sizing":"border-box"},".box-content":{"box-sizing":"content-box"}})},lineClamp:({matchUtilities:r,addUtilities:e,theme:t})=>{r({"line-clamp":i=>({overflow:"hidden",display:"-webkit-box","-webkit-box-orient":"vertical","-webkit-line-clamp":`${i}`})},{values:t("lineClamp")}),e({".line-clamp-none":{overflow:"visible",display:"block","-webkit-box-orient":"horizontal","-webkit-line-clamp":"none"}})},display:({addUtilities:r})=>{r({".block":{display:"block"},".inline-block":{display:"inline-block"},".inline":{display:"inline"},".flex":{display:"flex"},".inline-flex":{display:"inline-flex"},".table":{display:"table"},".inline-table":{display:"inline-table"},".table-caption":{display:"table-caption"},".table-cell":{display:"table-cell"},".table-column":{display:"table-column"},".table-column-group":{display:"table-column-group"},".table-footer-group":{display:"table-footer-group"},".table-header-group":{display:"table-header-group"},".table-row-group":{display:"table-row-group"},".table-row":{display:"table-row"},".flow-root":{display:"flow-root"},".grid":{display:"grid"},".inline-grid":{display:"inline-grid"},".contents":{display:"contents"},".list-item":{display:"list-item"},".hidden":{display:"none"}})},aspectRatio:L("aspectRatio",[["aspect",["aspect-ratio"]]]),size:L("size",[["size",["width","height"]]]),height:L("height",[["h",["height"]]]),maxHeight:L("maxHeight",[["max-h",["maxHeight"]]]),minHeight:L("minHeight",[["min-h",["minHeight"]]]),width:L("width",[["w",["width"]]]),minWidth:L("minWidth",[["min-w",["minWidth"]]]),maxWidth:L("maxWidth",[["max-w",["maxWidth"]]]),flex:L("flex"),flexShrink:L("flexShrink",[["flex-shrink",["flex-shrink"]],["shrink",["flex-shrink"]]]),flexGrow:L("flexGrow",[["flex-grow",["flex-grow"]],["grow",["flex-grow"]]]),flexBasis:L("flexBasis",[["basis",["flex-basis"]]]),tableLayout:({addUtilities:r})=>{r({".table-auto":{"table-layout":"auto"},".table-fixed":{"table-layout":"fixed"}})},captionSide:({addUtilities:r})=>{r({".caption-top":{"caption-side":"top"},".caption-bottom":{"caption-side":"bottom"}})},borderCollapse:({addUtilities:r})=>{r({".border-collapse":{"border-collapse":"collapse"},".border-separate":{"border-collapse":"separate"}})},borderSpacing:({addDefaults:r,matchUtilities:e,theme:t})=>{r("border-spacing",{"--tw-border-spacing-x":0,"--tw-border-spacing-y":0}),e({"border-spacing":i=>({"--tw-border-spacing-x":i,"--tw-border-spacing-y":i,"@defaults border-spacing":{},"border-spacing":"var(--tw-border-spacing-x) var(--tw-border-spacing-y)"}),"border-spacing-x":i=>({"--tw-border-spacing-x":i,"@defaults border-spacing":{},"border-spacing":"var(--tw-border-spacing-x) var(--tw-border-spacing-y)"}),"border-spacing-y":i=>({"--tw-border-spacing-y":i,"@defaults border-spacing":{},"border-spacing":"var(--tw-border-spacing-x) var(--tw-border-spacing-y)"})},{values:t("borderSpacing")})},transformOrigin:L("transformOrigin",[["origin",["transformOrigin"]]]),translate:L("translate",[[["translate-x",[["@defaults transform",{}],"--tw-translate-x",["transform",Xe]]],["translate-y",[["@defaults transform",{}],"--tw-translate-y",["transform",Xe]]]]],{supportsNegativeValues:!0}),rotate:L("rotate",[["rotate",[["@defaults transform",{}],"--tw-rotate",["transform",Xe]]]],{supportsNegativeValues:!0}),skew:L("skew",[[["skew-x",[["@defaults transform",{}],"--tw-skew-x",["transform",Xe]]],["skew-y",[["@defaults transform",{}],"--tw-skew-y",["transform",Xe]]]]],{supportsNegativeValues:!0}),scale:L("scale",[["scale",[["@defaults transform",{}],"--tw-scale-x","--tw-scale-y",["transform",Xe]]],[["scale-x",[["@defaults transform",{}],"--tw-scale-x",["transform",Xe]]],["scale-y",[["@defaults transform",{}],"--tw-scale-y",["transform",Xe]]]]],{supportsNegativeValues:!0}),transform:({addDefaults:r,addUtilities:e})=>{r("transform",{"--tw-translate-x":"0","--tw-translate-y":"0","--tw-rotate":"0","--tw-skew-x":"0","--tw-skew-y":"0","--tw-scale-x":"1","--tw-scale-y":"1"}),e({".transform":{"@defaults transform":{},transform:Xe},".transform-cpu":{transform:Xe},".transform-gpu":{transform:Xe.replace("translate(var(--tw-translate-x), var(--tw-translate-y))","translate3d(var(--tw-translate-x), var(--tw-translate-y), 0)")},".transform-none":{transform:"none"}})},animation:({matchUtilities:r,theme:e,config:t})=>{let i=s=>Te(t("prefix")+s),n=Object.fromEntries(Object.entries(e("keyframes")??{}).map(([s,a])=>[s,{[`@keyframes ${i(s)}`]:a}]));r({animate:s=>{let a=Mo(s);return[...a.flatMap(o=>n[o.name]),{animation:a.map(({name:o,value:l})=>o===void 0||n[o]===void 0?l:l.replace(o,i(o))).join(", ")}]}},{values:e("animation")})},cursor:L("cursor"),touchAction:({addDefaults:r,addUtilities:e})=>{r("touch-action",{"--tw-pan-x":" ","--tw-pan-y":" ","--tw-pinch-zoom":" "});let t="var(--tw-pan-x) var(--tw-pan-y) var(--tw-pinch-zoom)";e({".touch-auto":{"touch-action":"auto"},".touch-none":{"touch-action":"none"},".touch-pan-x":{"@defaults touch-action":{},"--tw-pan-x":"pan-x","touch-action":t},".touch-pan-left":{"@defaults touch-action":{},"--tw-pan-x":"pan-left","touch-action":t},".touch-pan-right":{"@defaults touch-action":{},"--tw-pan-x":"pan-right","touch-action":t},".touch-pan-y":{"@defaults touch-action":{},"--tw-pan-y":"pan-y","touch-action":t},".touch-pan-up":{"@defaults touch-action":{},"--tw-pan-y":"pan-up","touch-action":t},".touch-pan-down":{"@defaults touch-action":{},"--tw-pan-y":"pan-down","touch-action":t},".touch-pinch-zoom":{"@defaults touch-action":{},"--tw-pinch-zoom":"pinch-zoom","touch-action":t},".touch-manipulation":{"touch-action":"manipulation"}})},userSelect:({addUtilities:r})=>{r({".select-none":{"user-select":"none"},".select-text":{"user-select":"text"},".select-all":{"user-select":"all"},".select-auto":{"user-select":"auto"}})},resize:({addUtilities:r})=>{r({".resize-none":{resize:"none"},".resize-y":{resize:"vertical"},".resize-x":{resize:"horizontal"},".resize":{resize:"both"}})},scrollSnapType:({addDefaults:r,addUtilities:e})=>{r("scroll-snap-type",{"--tw-scroll-snap-strictness":"proximity"}),e({".snap-none":{"scroll-snap-type":"none"},".snap-x":{"@defaults scroll-snap-type":{},"scroll-snap-type":"x var(--tw-scroll-snap-strictness)"},".snap-y":{"@defaults scroll-snap-type":{},"scroll-snap-type":"y var(--tw-scroll-snap-strictness)"},".snap-both":{"@defaults scroll-snap-type":{},"scroll-snap-type":"both var(--tw-scroll-snap-strictness)"},".snap-mandatory":{"--tw-scroll-snap-strictness":"mandatory"},".snap-proximity":{"--tw-scroll-snap-strictness":"proximity"}})},scrollSnapAlign:({addUtilities:r})=>{r({".snap-start":{"scroll-snap-align":"start"},".snap-end":{"scroll-snap-align":"end"},".snap-center":{"scroll-snap-align":"center"},".snap-align-none":{"scroll-snap-align":"none"}})},scrollSnapStop:({addUtilities:r})=>{r({".snap-normal":{"scroll-snap-stop":"normal"},".snap-always":{"scroll-snap-stop":"always"}})},scrollMargin:L("scrollMargin",[["scroll-m",["scroll-margin"]],[["scroll-mx",["scroll-margin-left","scroll-margin-right"]],["scroll-my",["scroll-margin-top","scroll-margin-bottom"]]],[["scroll-ms",["scroll-margin-inline-start"]],["scroll-me",["scroll-margin-inline-end"]],["scroll-mt",["scroll-margin-top"]],["scroll-mr",["scroll-margin-right"]],["scroll-mb",["scroll-margin-bottom"]],["scroll-ml",["scroll-margin-left"]]]],{supportsNegativeValues:!0}),scrollPadding:L("scrollPadding",[["scroll-p",["scroll-padding"]],[["scroll-px",["scroll-padding-left","scroll-padding-right"]],["scroll-py",["scroll-padding-top","scroll-padding-bottom"]]],[["scroll-ps",["scroll-padding-inline-start"]],["scroll-pe",["scroll-padding-inline-end"]],["scroll-pt",["scroll-padding-top"]],["scroll-pr",["scroll-padding-right"]],["scroll-pb",["scroll-padding-bottom"]],["scroll-pl",["scroll-padding-left"]]]]),listStylePosition:({addUtilities:r})=>{r({".list-inside":{"list-style-position":"inside"},".list-outside":{"list-style-position":"outside"}})},listStyleType:L("listStyleType",[["list",["listStyleType"]]]),listStyleImage:L("listStyleImage",[["list-image",["listStyleImage"]]]),appearance:({addUtilities:r})=>{r({".appearance-none":{appearance:"none"},".appearance-auto":{appearance:"auto"}})},columns:L("columns",[["columns",["columns"]]]),breakBefore:({addUtilities:r})=>{r({".break-before-auto":{"break-before":"auto"},".break-before-avoid":{"break-before":"avoid"},".break-before-all":{"break-before":"all"},".break-before-avoid-page":{"break-before":"avoid-page"},".break-before-page":{"break-before":"page"},".break-before-left":{"break-before":"left"},".break-before-right":{"break-before":"right"},".break-before-column":{"break-before":"column"}})},breakInside:({addUtilities:r})=>{r({".break-inside-auto":{"break-inside":"auto"},".break-inside-avoid":{"break-inside":"avoid"},".break-inside-avoid-page":{"break-inside":"avoid-page"},".break-inside-avoid-column":{"break-inside":"avoid-column"}})},breakAfter:({addUtilities:r})=>{r({".break-after-auto":{"break-after":"auto"},".break-after-avoid":{"break-after":"avoid"},".break-after-all":{"break-after":"all"},".break-after-avoid-page":{"break-after":"avoid-page"},".break-after-page":{"break-after":"page"},".break-after-left":{"break-after":"left"},".break-after-right":{"break-after":"right"},".break-after-column":{"break-after":"column"}})},gridAutoColumns:L("gridAutoColumns",[["auto-cols",["gridAutoColumns"]]]),gridAutoFlow:({addUtilities:r})=>{r({".grid-flow-row":{gridAutoFlow:"row"},".grid-flow-col":{gridAutoFlow:"column"},".grid-flow-dense":{gridAutoFlow:"dense"},".grid-flow-row-dense":{gridAutoFlow:"row dense"},".grid-flow-col-dense":{gridAutoFlow:"column dense"}})},gridAutoRows:L("gridAutoRows",[["auto-rows",["gridAutoRows"]]]),gridTemplateColumns:L("gridTemplateColumns",[["grid-cols",["gridTemplateColumns"]]]),gridTemplateRows:L("gridTemplateRows",[["grid-rows",["gridTemplateRows"]]]),flexDirection:({addUtilities:r})=>{r({".flex-row":{"flex-direction":"row"},".flex-row-reverse":{"flex-direction":"row-reverse"},".flex-col":{"flex-direction":"column"},".flex-col-reverse":{"flex-direction":"column-reverse"}})},flexWrap:({addUtilities:r})=>{r({".flex-wrap":{"flex-wrap":"wrap"},".flex-wrap-reverse":{"flex-wrap":"wrap-reverse"},".flex-nowrap":{"flex-wrap":"nowrap"}})},placeContent:({addUtilities:r})=>{r({".place-content-center":{"place-content":"center"},".place-content-start":{"place-content":"start"},".place-content-end":{"place-content":"end"},".place-content-between":{"place-content":"space-between"},".place-content-around":{"place-content":"space-around"},".place-content-evenly":{"place-content":"space-evenly"},".place-content-baseline":{"place-content":"baseline"},".place-content-stretch":{"place-content":"stretch"}})},placeItems:({addUtilities:r})=>{r({".place-items-start":{"place-items":"start"},".place-items-end":{"place-items":"end"},".place-items-center":{"place-items":"center"},".place-items-baseline":{"place-items":"baseline"},".place-items-stretch":{"place-items":"stretch"}})},alignContent:({addUtilities:r})=>{r({".content-normal":{"align-content":"normal"},".content-center":{"align-content":"center"},".content-start":{"align-content":"flex-start"},".content-end":{"align-content":"flex-end"},".content-between":{"align-content":"space-between"},".content-around":{"align-content":"space-around"},".content-evenly":{"align-content":"space-evenly"},".content-baseline":{"align-content":"baseline"},".content-stretch":{"align-content":"stretch"}})},alignItems:({addUtilities:r})=>{r({".items-start":{"align-items":"flex-start"},".items-end":{"align-items":"flex-end"},".items-center":{"align-items":"center"},".items-baseline":{"align-items":"baseline"},".items-stretch":{"align-items":"stretch"}})},justifyContent:({addUtilities:r})=>{r({".justify-normal":{"justify-content":"normal"},".justify-start":{"justify-content":"flex-start"},".justify-end":{"justify-content":"flex-end"},".justify-center":{"justify-content":"center"},".justify-between":{"justify-content":"space-between"},".justify-around":{"justify-content":"space-around"},".justify-evenly":{"justify-content":"space-evenly"},".justify-stretch":{"justify-content":"stretch"}})},justifyItems:({addUtilities:r})=>{r({".justify-items-start":{"justify-items":"start"},".justify-items-end":{"justify-items":"end"},".justify-items-center":{"justify-items":"center"},".justify-items-stretch":{"justify-items":"stretch"}})},gap:L("gap",[["gap",["gap"]],[["gap-x",["columnGap"]],["gap-y",["rowGap"]]]]),space:({matchUtilities:r,addUtilities:e,theme:t})=>{r({"space-x":i=>(i=i==="0"?"0px":i,{"& > :not([hidden]) ~ :not([hidden])":{"--tw-space-x-reverse":"0","margin-right":`calc(${i} * var(--tw-space-x-reverse))`,"margin-left":`calc(${i} * calc(1 - var(--tw-space-x-reverse)))`}}),"space-y":i=>(i=i==="0"?"0px":i,{"& > :not([hidden]) ~ :not([hidden])":{"--tw-space-y-reverse":"0","margin-top":`calc(${i} * calc(1 - var(--tw-space-y-reverse)))`,"margin-bottom":`calc(${i} * var(--tw-space-y-reverse))`}})},{values:t("space"),supportsNegativeValues:!0}),e({".space-y-reverse > :not([hidden]) ~ :not([hidden])":{"--tw-space-y-reverse":"1"},".space-x-reverse > :not([hidden]) ~ :not([hidden])":{"--tw-space-x-reverse":"1"}})},divideWidth:({matchUtilities:r,addUtilities:e,theme:t})=>{r({"divide-x":i=>(i=i==="0"?"0px":i,{"& > :not([hidden]) ~ :not([hidden])":{"@defaults border-width":{},"--tw-divide-x-reverse":"0","border-right-width":`calc(${i} * var(--tw-divide-x-reverse))`,"border-left-width":`calc(${i} * calc(1 - var(--tw-divide-x-reverse)))`}}),"divide-y":i=>(i=i==="0"?"0px":i,{"& > :not([hidden]) ~ :not([hidden])":{"@defaults border-width":{},"--tw-divide-y-reverse":"0","border-top-width":`calc(${i} * calc(1 - var(--tw-divide-y-reverse)))`,"border-bottom-width":`calc(${i} * var(--tw-divide-y-reverse))`}})},{values:t("divideWidth"),type:["line-width","length","any"]}),e({".divide-y-reverse > :not([hidden]) ~ :not([hidden])":{"@defaults border-width":{},"--tw-divide-y-reverse":"1"},".divide-x-reverse > :not([hidden]) ~ :not([hidden])":{"@defaults border-width":{},"--tw-divide-x-reverse":"1"}})},divideStyle:({addUtilities:r})=>{r({".divide-solid > :not([hidden]) ~ :not([hidden])":{"border-style":"solid"},".divide-dashed > :not([hidden]) ~ :not([hidden])":{"border-style":"dashed"},".divide-dotted > :not([hidden]) ~ :not([hidden])":{"border-style":"dotted"},".divide-double > :not([hidden]) ~ :not([hidden])":{"border-style":"double"},".divide-none > :not([hidden]) ~ :not([hidden])":{"border-style":"none"}})},divideColor:({matchUtilities:r,theme:e,corePlugins:t})=>{r({divide:i=>t("divideOpacity")?{["& > :not([hidden]) ~ :not([hidden])"]:Ae({color:i,property:"border-color",variable:"--tw-divide-opacity"})}:{["& > :not([hidden]) ~ :not([hidden])"]:{"border-color":X(i)}}},{values:(({DEFAULT:i,...n})=>n)(xe(e("divideColor"))),type:["color","any"]})},divideOpacity:({matchUtilities:r,theme:e})=>{r({"divide-opacity":t=>({["& > :not([hidden]) ~ :not([hidden])"]:{"--tw-divide-opacity":t}})},{values:e("divideOpacity")})},placeSelf:({addUtilities:r})=>{r({".place-self-auto":{"place-self":"auto"},".place-self-start":{"place-self":"start"},".place-self-end":{"place-self":"end"},".place-self-center":{"place-self":"center"},".place-self-stretch":{"place-self":"stretch"}})},alignSelf:({addUtilities:r})=>{r({".self-auto":{"align-self":"auto"},".self-start":{"align-self":"flex-start"},".self-end":{"align-self":"flex-end"},".self-center":{"align-self":"center"},".self-stretch":{"align-self":"stretch"},".self-baseline":{"align-self":"baseline"}})},justifySelf:({addUtilities:r})=>{r({".justify-self-auto":{"justify-self":"auto"},".justify-self-start":{"justify-self":"start"},".justify-self-end":{"justify-self":"end"},".justify-self-center":{"justify-self":"center"},".justify-self-stretch":{"justify-self":"stretch"}})},overflow:({addUtilities:r})=>{r({".overflow-auto":{overflow:"auto"},".overflow-hidden":{overflow:"hidden"},".overflow-clip":{overflow:"clip"},".overflow-visible":{overflow:"visible"},".overflow-scroll":{overflow:"scroll"},".overflow-x-auto":{"overflow-x":"auto"},".overflow-y-auto":{"overflow-y":"auto"},".overflow-x-hidden":{"overflow-x":"hidden"},".overflow-y-hidden":{"overflow-y":"hidden"},".overflow-x-clip":{"overflow-x":"clip"},".overflow-y-clip":{"overflow-y":"clip"},".overflow-x-visible":{"overflow-x":"visible"},".overflow-y-visible":{"overflow-y":"visible"},".overflow-x-scroll":{"overflow-x":"scroll"},".overflow-y-scroll":{"overflow-y":"scroll"}})},overscrollBehavior:({addUtilities:r})=>{r({".overscroll-auto":{"overscroll-behavior":"auto"},".overscroll-contain":{"overscroll-behavior":"contain"},".overscroll-none":{"overscroll-behavior":"none"},".overscroll-y-auto":{"overscroll-behavior-y":"auto"},".overscroll-y-contain":{"overscroll-behavior-y":"contain"},".overscroll-y-none":{"overscroll-behavior-y":"none"},".overscroll-x-auto":{"overscroll-behavior-x":"auto"},".overscroll-x-contain":{"overscroll-behavior-x":"contain"},".overscroll-x-none":{"overscroll-behavior-x":"none"}})},scrollBehavior:({addUtilities:r})=>{r({".scroll-auto":{"scroll-behavior":"auto"},".scroll-smooth":{"scroll-behavior":"smooth"}})},textOverflow:({addUtilities:r})=>{r({".truncate":{overflow:"hidden","text-overflow":"ellipsis","white-space":"nowrap"},".overflow-ellipsis":{"text-overflow":"ellipsis"},".text-ellipsis":{"text-overflow":"ellipsis"},".text-clip":{"text-overflow":"clip"}})},hyphens:({addUtilities:r})=>{r({".hyphens-none":{hyphens:"none"},".hyphens-manual":{hyphens:"manual"},".hyphens-auto":{hyphens:"auto"}})},whitespace:({addUtilities:r})=>{r({".whitespace-normal":{"white-space":"normal"},".whitespace-nowrap":{"white-space":"nowrap"},".whitespace-pre":{"white-space":"pre"},".whitespace-pre-line":{"white-space":"pre-line"},".whitespace-pre-wrap":{"white-space":"pre-wrap"},".whitespace-break-spaces":{"white-space":"break-spaces"}})},textWrap:({addUtilities:r})=>{r({".text-wrap":{"text-wrap":"wrap"},".text-nowrap":{"text-wrap":"nowrap"},".text-balance":{"text-wrap":"balance"},".text-pretty":{"text-wrap":"pretty"}})},wordBreak:({addUtilities:r})=>{r({".break-normal":{"overflow-wrap":"normal","word-break":"normal"},".break-words":{"overflow-wrap":"break-word"},".break-all":{"word-break":"break-all"},".break-keep":{"word-break":"keep-all"}})},borderRadius:L("borderRadius",[["rounded",["border-radius"]],[["rounded-s",["border-start-start-radius","border-end-start-radius"]],["rounded-e",["border-start-end-radius","border-end-end-radius"]],["rounded-t",["border-top-left-radius","border-top-right-radius"]],["rounded-r",["border-top-right-radius","border-bottom-right-radius"]],["rounded-b",["border-bottom-right-radius","border-bottom-left-radius"]],["rounded-l",["border-top-left-radius","border-bottom-left-radius"]]],[["rounded-ss",["border-start-start-radius"]],["rounded-se",["border-start-end-radius"]],["rounded-ee",["border-end-end-radius"]],["rounded-es",["border-end-start-radius"]],["rounded-tl",["border-top-left-radius"]],["rounded-tr",["border-top-right-radius"]],["rounded-br",["border-bottom-right-radius"]],["rounded-bl",["border-bottom-left-radius"]]]]),borderWidth:L("borderWidth",[["border",[["@defaults border-width",{}],"border-width"]],[["border-x",[["@defaults border-width",{}],"border-left-width","border-right-width"]],["border-y",[["@defaults border-width",{}],"border-top-width","border-bottom-width"]]],[["border-s",[["@defaults border-width",{}],"border-inline-start-width"]],["border-e",[["@defaults border-width",{}],"border-inline-end-width"]],["border-t",[["@defaults border-width",{}],"border-top-width"]],["border-r",[["@defaults border-width",{}],"border-right-width"]],["border-b",[["@defaults border-width",{}],"border-bottom-width"]],["border-l",[["@defaults border-width",{}],"border-left-width"]]]],{type:["line-width","length"]}),borderStyle:({addUtilities:r})=>{r({".border-solid":{"border-style":"solid"},".border-dashed":{"border-style":"dashed"},".border-dotted":{"border-style":"dotted"},".border-double":{"border-style":"double"},".border-hidden":{"border-style":"hidden"},".border-none":{"border-style":"none"}})},borderColor:({matchUtilities:r,theme:e,corePlugins:t})=>{r({border:i=>t("borderOpacity")?Ae({color:i,property:"border-color",variable:"--tw-border-opacity"}):{"border-color":X(i)}},{values:(({DEFAULT:i,...n})=>n)(xe(e("borderColor"))),type:["color","any"]}),r({"border-x":i=>t("borderOpacity")?Ae({color:i,property:["border-left-color","border-right-color"],variable:"--tw-border-opacity"}):{"border-left-color":X(i),"border-right-color":X(i)},"border-y":i=>t("borderOpacity")?Ae({color:i,property:["border-top-color","border-bottom-color"],variable:"--tw-border-opacity"}):{"border-top-color":X(i),"border-bottom-color":X(i)}},{values:(({DEFAULT:i,...n})=>n)(xe(e("borderColor"))),type:["color","any"]}),r({"border-s":i=>t("borderOpacity")?Ae({color:i,property:"border-inline-start-color",variable:"--tw-border-opacity"}):{"border-inline-start-color":X(i)},"border-e":i=>t("borderOpacity")?Ae({color:i,property:"border-inline-end-color",variable:"--tw-border-opacity"}):{"border-inline-end-color":X(i)},"border-t":i=>t("borderOpacity")?Ae({color:i,property:"border-top-color",variable:"--tw-border-opacity"}):{"border-top-color":X(i)},"border-r":i=>t("borderOpacity")?Ae({color:i,property:"border-right-color",variable:"--tw-border-opacity"}):{"border-right-color":X(i)},"border-b":i=>t("borderOpacity")?Ae({color:i,property:"border-bottom-color",variable:"--tw-border-opacity"}):{"border-bottom-color":X(i)},"border-l":i=>t("borderOpacity")?Ae({color:i,property:"border-left-color",variable:"--tw-border-opacity"}):{"border-left-color":X(i)}},{values:(({DEFAULT:i,...n})=>n)(xe(e("borderColor"))),type:["color","any"]})},borderOpacity:L("borderOpacity",[["border-opacity",["--tw-border-opacity"]]]),backgroundColor:({matchUtilities:r,theme:e,corePlugins:t})=>{r({bg:i=>t("backgroundOpacity")?Ae({color:i,property:"background-color",variable:"--tw-bg-opacity"}):{"background-color":X(i)}},{values:xe(e("backgroundColor")),type:["color","any"]})},backgroundOpacity:L("backgroundOpacity",[["bg-opacity",["--tw-bg-opacity"]]]),backgroundImage:L("backgroundImage",[["bg",["background-image"]]],{type:["lookup","image","url"]}),gradientColorStops:(()=>{function r(e){return Je(e,0,"rgb(255 255 255 / 0)")}return function({matchUtilities:e,theme:t,addDefaults:i}){i("gradient-color-stops",{"--tw-gradient-from-position":" ","--tw-gradient-via-position":" ","--tw-gradient-to-position":" "});let n={values:xe(t("gradientColorStops")),type:["color","any"]},s={values:t("gradientColorStopPositions"),type:["length","percentage"]};e({from:a=>{let o=r(a);return{"@defaults gradient-color-stops":{},"--tw-gradient-from":`${X(a)} var(--tw-gradient-from-position)`,"--tw-gradient-to":`${o} var(--tw-gradient-to-position)`,"--tw-gradient-stops":"var(--tw-gradient-from), var(--tw-gradient-to)"}}},n),e({from:a=>({"--tw-gradient-from-position":a})},s),e({via:a=>{let o=r(a);return{"@defaults gradient-color-stops":{},"--tw-gradient-to":`${o} var(--tw-gradient-to-position)`,"--tw-gradient-stops":`var(--tw-gradient-from), ${X(a)} var(--tw-gradient-via-position), var(--tw-gradient-to)`}}},n),e({via:a=>({"--tw-gradient-via-position":a})},s),e({to:a=>({"@defaults gradient-color-stops":{},"--tw-gradient-to":`${X(a)} var(--tw-gradient-to-position)`})},n),e({to:a=>({"--tw-gradient-to-position":a})},s)}})(),boxDecorationBreak:({addUtilities:r})=>{r({".decoration-slice":{"box-decoration-break":"slice"},".decoration-clone":{"box-decoration-break":"clone"},".box-decoration-slice":{"box-decoration-break":"slice"},".box-decoration-clone":{"box-decoration-break":"clone"}})},backgroundSize:L("backgroundSize",[["bg",["background-size"]]],{type:["lookup","length","percentage","size"]}),backgroundAttachment:({addUtilities:r})=>{r({".bg-fixed":{"background-attachment":"fixed"},".bg-local":{"background-attachment":"local"},".bg-scroll":{"background-attachment":"scroll"}})},backgroundClip:({addUtilities:r})=>{r({".bg-clip-border":{"background-clip":"border-box"},".bg-clip-padding":{"background-clip":"padding-box"},".bg-clip-content":{"background-clip":"content-box"},".bg-clip-text":{"background-clip":"text"}})},backgroundPosition:L("backgroundPosition",[["bg",["background-position"]]],{type:["lookup",["position",{preferOnConflict:!0}]]}),backgroundRepeat:({addUtilities:r})=>{r({".bg-repeat":{"background-repeat":"repeat"},".bg-no-repeat":{"background-repeat":"no-repeat"},".bg-repeat-x":{"background-repeat":"repeat-x"},".bg-repeat-y":{"background-repeat":"repeat-y"},".bg-repeat-round":{"background-repeat":"round"},".bg-repeat-space":{"background-repeat":"space"}})},backgroundOrigin:({addUtilities:r})=>{r({".bg-origin-border":{"background-origin":"border-box"},".bg-origin-padding":{"background-origin":"padding-box"},".bg-origin-content":{"background-origin":"content-box"}})},fill:({matchUtilities:r,theme:e})=>{r({fill:t=>({fill:X(t)})},{values:xe(e("fill")),type:["color","any"]})},stroke:({matchUtilities:r,theme:e})=>{r({stroke:t=>({stroke:X(t)})},{values:xe(e("stroke")),type:["color","url","any"]})},strokeWidth:L("strokeWidth",[["stroke",["stroke-width"]]],{type:["length","number","percentage"]}),objectFit:({addUtilities:r})=>{r({".object-contain":{"object-fit":"contain"},".object-cover":{"object-fit":"cover"},".object-fill":{"object-fit":"fill"},".object-none":{"object-fit":"none"},".object-scale-down":{"object-fit":"scale-down"}})},objectPosition:L("objectPosition",[["object",["object-position"]]]),padding:L("padding",[["p",["padding"]],[["px",["padding-left","padding-right"]],["py",["padding-top","padding-bottom"]]],[["ps",["padding-inline-start"]],["pe",["padding-inline-end"]],["pt",["padding-top"]],["pr",["padding-right"]],["pb",["padding-bottom"]],["pl",["padding-left"]]]]),textAlign:({addUtilities:r})=>{r({".text-left":{"text-align":"left"},".text-center":{"text-align":"center"},".text-right":{"text-align":"right"},".text-justify":{"text-align":"justify"},".text-start":{"text-align":"start"},".text-end":{"text-align":"end"}})},textIndent:L("textIndent",[["indent",["text-indent"]]],{supportsNegativeValues:!0}),verticalAlign:({addUtilities:r,matchUtilities:e})=>{r({".align-baseline":{"vertical-align":"baseline"},".align-top":{"vertical-align":"top"},".align-middle":{"vertical-align":"middle"},".align-bottom":{"vertical-align":"bottom"},".align-text-top":{"vertical-align":"text-top"},".align-text-bottom":{"vertical-align":"text-bottom"},".align-sub":{"vertical-align":"sub"},".align-super":{"vertical-align":"super"}}),e({align:t=>({"vertical-align":t})})},fontFamily:({matchUtilities:r,theme:e})=>{r({font:t=>{let[i,n={}]=Array.isArray(t)&&ke(t[1])?t:[t],{fontFeatureSettings:s,fontVariationSettings:a}=n;return{"font-family":Array.isArray(i)?i.join(", "):i,...s===void 0?{}:{"font-feature-settings":s},...a===void 0?{}:{"font-variation-settings":a}}}},{values:e("fontFamily"),type:["lookup","generic-name","family-name"]})},fontSize:({matchUtilities:r,theme:e})=>{r({text:(t,{modifier:i})=>{let[n,s]=Array.isArray(t)?t:[t];if(i)return{"font-size":n,"line-height":i};let{lineHeight:a,letterSpacing:o,fontWeight:l}=ke(s)?s:{lineHeight:s};return{"font-size":n,...a===void 0?{}:{"line-height":a},...o===void 0?{}:{"letter-spacing":o},...l===void 0?{}:{"font-weight":l}}}},{values:e("fontSize"),modifiers:e("lineHeight"),type:["absolute-size","relative-size","length","percentage"]})},fontWeight:L("fontWeight",[["font",["fontWeight"]]],{type:["lookup","number","any"]}),textTransform:({addUtilities:r})=>{r({".uppercase":{"text-transform":"uppercase"},".lowercase":{"text-transform":"lowercase"},".capitalize":{"text-transform":"capitalize"},".normal-case":{"text-transform":"none"}})},fontStyle:({addUtilities:r})=>{r({".italic":{"font-style":"italic"},".not-italic":{"font-style":"normal"}})},fontVariantNumeric:({addDefaults:r,addUtilities:e})=>{let t="var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)";r("font-variant-numeric",{"--tw-ordinal":" ","--tw-slashed-zero":" ","--tw-numeric-figure":" ","--tw-numeric-spacing":" ","--tw-numeric-fraction":" "}),e({".normal-nums":{"font-variant-numeric":"normal"},".ordinal":{"@defaults font-variant-numeric":{},"--tw-ordinal":"ordinal","font-variant-numeric":t},".slashed-zero":{"@defaults font-variant-numeric":{},"--tw-slashed-zero":"slashed-zero","font-variant-numeric":t},".lining-nums":{"@defaults font-variant-numeric":{},"--tw-numeric-figure":"lining-nums","font-variant-numeric":t},".oldstyle-nums":{"@defaults font-variant-numeric":{},"--tw-numeric-figure":"oldstyle-nums","font-variant-numeric":t},".proportional-nums":{"@defaults font-variant-numeric":{},"--tw-numeric-spacing":"proportional-nums","font-variant-numeric":t},".tabular-nums":{"@defaults font-variant-numeric":{},"--tw-numeric-spacing":"tabular-nums","font-variant-numeric":t},".diagonal-fractions":{"@defaults font-variant-numeric":{},"--tw-numeric-fraction":"diagonal-fractions","font-variant-numeric":t},".stacked-fractions":{"@defaults font-variant-numeric":{},"--tw-numeric-fraction":"stacked-fractions","font-variant-numeric":t}})},lineHeight:L("lineHeight",[["leading",["lineHeight"]]]),letterSpacing:L("letterSpacing",[["tracking",["letterSpacing"]]],{supportsNegativeValues:!0}),textColor:({matchUtilities:r,theme:e,corePlugins:t})=>{r({text:i=>t("textOpacity")?Ae({color:i,property:"color",variable:"--tw-text-opacity"}):{color:X(i)}},{values:xe(e("textColor")),type:["color","any"]})},textOpacity:L("textOpacity",[["text-opacity",["--tw-text-opacity"]]]),textDecoration:({addUtilities:r})=>{r({".underline":{"text-decoration-line":"underline"},".overline":{"text-decoration-line":"overline"},".line-through":{"text-decoration-line":"line-through"},".no-underline":{"text-decoration-line":"none"}})},textDecorationColor:({matchUtilities:r,theme:e})=>{r({decoration:t=>({"text-decoration-color":X(t)})},{values:xe(e("textDecorationColor")),type:["color","any"]})},textDecorationStyle:({addUtilities:r})=>{r({".decoration-solid":{"text-decoration-style":"solid"},".decoration-double":{"text-decoration-style":"double"},".decoration-dotted":{"text-decoration-style":"dotted"},".decoration-dashed":{"text-decoration-style":"dashed"},".decoration-wavy":{"text-decoration-style":"wavy"}})},textDecorationThickness:L("textDecorationThickness",[["decoration",["text-decoration-thickness"]]],{type:["length","percentage"]}),textUnderlineOffset:L("textUnderlineOffset",[["underline-offset",["text-underline-offset"]]],{type:["length","percentage","any"]}),fontSmoothing:({addUtilities:r})=>{r({".antialiased":{"-webkit-font-smoothing":"antialiased","-moz-osx-font-smoothing":"grayscale"},".subpixel-antialiased":{"-webkit-font-smoothing":"auto","-moz-osx-font-smoothing":"auto"}})},placeholderColor:({matchUtilities:r,theme:e,corePlugins:t})=>{r({placeholder:i=>t("placeholderOpacity")?{"&::placeholder":Ae({color:i,property:"color",variable:"--tw-placeholder-opacity"})}:{"&::placeholder":{color:X(i)}}},{values:xe(e("placeholderColor")),type:["color","any"]})},placeholderOpacity:({matchUtilities:r,theme:e})=>{r({"placeholder-opacity":t=>({["&::placeholder"]:{"--tw-placeholder-opacity":t}})},{values:e("placeholderOpacity")})},caretColor:({matchUtilities:r,theme:e})=>{r({caret:t=>({"caret-color":X(t)})},{values:xe(e("caretColor")),type:["color","any"]})},accentColor:({matchUtilities:r,theme:e})=>{r({accent:t=>({"accent-color":X(t)})},{values:xe(e("accentColor")),type:["color","any"]})},opacity:L("opacity",[["opacity",["opacity"]]]),backgroundBlendMode:({addUtilities:r})=>{r({".bg-blend-normal":{"background-blend-mode":"normal"},".bg-blend-multiply":{"background-blend-mode":"multiply"},".bg-blend-screen":{"background-blend-mode":"screen"},".bg-blend-overlay":{"background-blend-mode":"overlay"},".bg-blend-darken":{"background-blend-mode":"darken"},".bg-blend-lighten":{"background-blend-mode":"lighten"},".bg-blend-color-dodge":{"background-blend-mode":"color-dodge"},".bg-blend-color-burn":{"background-blend-mode":"color-burn"},".bg-blend-hard-light":{"background-blend-mode":"hard-light"},".bg-blend-soft-light":{"background-blend-mode":"soft-light"},".bg-blend-difference":{"background-blend-mode":"difference"},".bg-blend-exclusion":{"background-blend-mode":"exclusion"},".bg-blend-hue":{"background-blend-mode":"hue"},".bg-blend-saturation":{"background-blend-mode":"saturation"},".bg-blend-color":{"background-blend-mode":"color"},".bg-blend-luminosity":{"background-blend-mode":"luminosity"}})},mixBlendMode:({addUtilities:r})=>{r({".mix-blend-normal":{"mix-blend-mode":"normal"},".mix-blend-multiply":{"mix-blend-mode":"multiply"},".mix-blend-screen":{"mix-blend-mode":"screen"},".mix-blend-overlay":{"mix-blend-mode":"overlay"},".mix-blend-darken":{"mix-blend-mode":"darken"},".mix-blend-lighten":{"mix-blend-mode":"lighten"},".mix-blend-color-dodge":{"mix-blend-mode":"color-dodge"},".mix-blend-color-burn":{"mix-blend-mode":"color-burn"},".mix-blend-hard-light":{"mix-blend-mode":"hard-light"},".mix-blend-soft-light":{"mix-blend-mode":"soft-light"},".mix-blend-difference":{"mix-blend-mode":"difference"},".mix-blend-exclusion":{"mix-blend-mode":"exclusion"},".mix-blend-hue":{"mix-blend-mode":"hue"},".mix-blend-saturation":{"mix-blend-mode":"saturation"},".mix-blend-color":{"mix-blend-mode":"color"},".mix-blend-luminosity":{"mix-blend-mode":"luminosity"},".mix-blend-plus-darker":{"mix-blend-mode":"plus-darker"},".mix-blend-plus-lighter":{"mix-blend-mode":"plus-lighter"}})},boxShadow:(()=>{let r=mt("boxShadow"),e=["var(--tw-ring-offset-shadow, 0 0 #0000)","var(--tw-ring-shadow, 0 0 #0000)","var(--tw-shadow)"].join(", ");return function({matchUtilities:t,addDefaults:i,theme:n}){i("box-shadow",{"--tw-ring-offset-shadow":"0 0 #0000","--tw-ring-shadow":"0 0 #0000","--tw-shadow":"0 0 #0000","--tw-shadow-colored":"0 0 #0000"}),t({shadow:s=>{s=r(s);let a=en(s);for(let o of a)!o.valid||(o.color="var(--tw-shadow-color)");return{"@defaults box-shadow":{},"--tw-shadow":s==="none"?"0 0 #0000":s,"--tw-shadow-colored":s==="none"?"0 0 #0000":Lf(a),"box-shadow":e}}},{values:n("boxShadow"),type:["shadow"]})}})(),boxShadowColor:({matchUtilities:r,theme:e})=>{r({shadow:t=>({"--tw-shadow-color":X(t),"--tw-shadow":"var(--tw-shadow-colored)"})},{values:xe(e("boxShadowColor")),type:["color","any"]})},outlineStyle:({addUtilities:r})=>{r({".outline-none":{outline:"2px solid transparent","outline-offset":"2px"},".outline":{"outline-style":"solid"},".outline-dashed":{"outline-style":"dashed"},".outline-dotted":{"outline-style":"dotted"},".outline-double":{"outline-style":"double"}})},outlineWidth:L("outlineWidth",[["outline",["outline-width"]]],{type:["length","number","percentage"]}),outlineOffset:L("outlineOffset",[["outline-offset",["outline-offset"]]],{type:["length","number","percentage","any"],supportsNegativeValues:!0}),outlineColor:({matchUtilities:r,theme:e})=>{r({outline:t=>({"outline-color":X(t)})},{values:xe(e("outlineColor")),type:["color","any"]})},ringWidth:({matchUtilities:r,addDefaults:e,addUtilities:t,theme:i,config:n})=>{let s=(()=>{if(we(n(),"respectDefaultRingColorOpacity"))return i("ringColor.DEFAULT");let a=i("ringOpacity.DEFAULT","0.5");return i("ringColor")?.DEFAULT?Je(i("ringColor")?.DEFAULT,a,`rgb(147 197 253 / ${a})`):`rgb(147 197 253 / ${a})`})();e("ring-width",{"--tw-ring-inset":" ","--tw-ring-offset-width":i("ringOffsetWidth.DEFAULT","0px"),"--tw-ring-offset-color":i("ringOffsetColor.DEFAULT","#fff"),"--tw-ring-color":s,"--tw-ring-offset-shadow":"0 0 #0000","--tw-ring-shadow":"0 0 #0000","--tw-shadow":"0 0 #0000","--tw-shadow-colored":"0 0 #0000"}),r({ring:a=>({"@defaults ring-width":{},"--tw-ring-offset-shadow":"var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)","--tw-ring-shadow":`var(--tw-ring-inset) 0 0 0 calc(${a} + var(--tw-ring-offset-width)) var(--tw-ring-color)`,"box-shadow":["var(--tw-ring-offset-shadow)","var(--tw-ring-shadow)","var(--tw-shadow, 0 0 #0000)"].join(", ")})},{values:i("ringWidth"),type:"length"}),t({".ring-inset":{"@defaults ring-width":{},"--tw-ring-inset":"inset"}})},ringColor:({matchUtilities:r,theme:e,corePlugins:t})=>{r({ring:i=>t("ringOpacity")?Ae({color:i,property:"--tw-ring-color",variable:"--tw-ring-opacity"}):{"--tw-ring-color":X(i)}},{values:Object.fromEntries(Object.entries(xe(e("ringColor"))).filter(([i])=>i!=="DEFAULT")),type:["color","any"]})},ringOpacity:r=>{let{config:e}=r;return L("ringOpacity",[["ring-opacity",["--tw-ring-opacity"]]],{filterDefault:!we(e(),"respectDefaultRingColorOpacity")})(r)},ringOffsetWidth:L("ringOffsetWidth",[["ring-offset",["--tw-ring-offset-width"]]],{type:"length"}),ringOffsetColor:({matchUtilities:r,theme:e})=>{r({"ring-offset":t=>({"--tw-ring-offset-color":X(t)})},{values:xe(e("ringOffsetColor")),type:["color","any"]})},blur:({matchUtilities:r,theme:e})=>{r({blur:t=>({"--tw-blur":t.trim()===""?" ":`blur(${t})`,"@defaults filter":{},filter:nt})},{values:e("blur")})},brightness:({matchUtilities:r,theme:e})=>{r({brightness:t=>({"--tw-brightness":`brightness(${t})`,"@defaults filter":{},filter:nt})},{values:e("brightness")})},contrast:({matchUtilities:r,theme:e})=>{r({contrast:t=>({"--tw-contrast":`contrast(${t})`,"@defaults filter":{},filter:nt})},{values:e("contrast")})},dropShadow:({matchUtilities:r,theme:e})=>{r({"drop-shadow":t=>({"--tw-drop-shadow":Array.isArray(t)?t.map(i=>`drop-shadow(${i})`).join(" "):`drop-shadow(${t})`,"@defaults filter":{},filter:nt})},{values:e("dropShadow")})},grayscale:({matchUtilities:r,theme:e})=>{r({grayscale:t=>({"--tw-grayscale":`grayscale(${t})`,"@defaults filter":{},filter:nt})},{values:e("grayscale")})},hueRotate:({matchUtilities:r,theme:e})=>{r({"hue-rotate":t=>({"--tw-hue-rotate":`hue-rotate(${t})`,"@defaults filter":{},filter:nt})},{values:e("hueRotate"),supportsNegativeValues:!0})},invert:({matchUtilities:r,theme:e})=>{r({invert:t=>({"--tw-invert":`invert(${t})`,"@defaults filter":{},filter:nt})},{values:e("invert")})},saturate:({matchUtilities:r,theme:e})=>{r({saturate:t=>({"--tw-saturate":`saturate(${t})`,"@defaults filter":{},filter:nt})},{values:e("saturate")})},sepia:({matchUtilities:r,theme:e})=>{r({sepia:t=>({"--tw-sepia":`sepia(${t})`,"@defaults filter":{},filter:nt})},{values:e("sepia")})},filter:({addDefaults:r,addUtilities:e})=>{r("filter",{"--tw-blur":" ","--tw-brightness":" ","--tw-contrast":" ","--tw-grayscale":" ","--tw-hue-rotate":" ","--tw-invert":" ","--tw-saturate":" ","--tw-sepia":" ","--tw-drop-shadow":" "}),e({".filter":{"@defaults filter":{},filter:nt},".filter-none":{filter:"none"}})},backdropBlur:({matchUtilities:r,theme:e})=>{r({"backdrop-blur":t=>({"--tw-backdrop-blur":t.trim()===""?" ":`blur(${t})`,"@defaults backdrop-filter":{},"-webkit-backdrop-filter":ge,"backdrop-filter":ge})},{values:e("backdropBlur")})},backdropBrightness:({matchUtilities:r,theme:e})=>{r({"backdrop-brightness":t=>({"--tw-backdrop-brightness":`brightness(${t})`,"@defaults backdrop-filter":{},"-webkit-backdrop-filter":ge,"backdrop-filter":ge})},{values:e("backdropBrightness")})},backdropContrast:({matchUtilities:r,theme:e})=>{r({"backdrop-contrast":t=>({"--tw-backdrop-contrast":`contrast(${t})`,"@defaults backdrop-filter":{},"-webkit-backdrop-filter":ge,"backdrop-filter":ge})},{values:e("backdropContrast")})},backdropGrayscale:({matchUtilities:r,theme:e})=>{r({"backdrop-grayscale":t=>({"--tw-backdrop-grayscale":`grayscale(${t})`,"@defaults backdrop-filter":{},"-webkit-backdrop-filter":ge,"backdrop-filter":ge})},{values:e("backdropGrayscale")})},backdropHueRotate:({matchUtilities:r,theme:e})=>{r({"backdrop-hue-rotate":t=>({"--tw-backdrop-hue-rotate":`hue-rotate(${t})`,"@defaults backdrop-filter":{},"-webkit-backdrop-filter":ge,"backdrop-filter":ge})},{values:e("backdropHueRotate"),supportsNegativeValues:!0})},backdropInvert:({matchUtilities:r,theme:e})=>{r({"backdrop-invert":t=>({"--tw-backdrop-invert":`invert(${t})`,"@defaults backdrop-filter":{},"-webkit-backdrop-filter":ge,"backdrop-filter":ge})},{values:e("backdropInvert")})},backdropOpacity:({matchUtilities:r,theme:e})=>{r({"backdrop-opacity":t=>({"--tw-backdrop-opacity":`opacity(${t})`,"@defaults backdrop-filter":{},"-webkit-backdrop-filter":ge,"backdrop-filter":ge})},{values:e("backdropOpacity")})},backdropSaturate:({matchUtilities:r,theme:e})=>{r({"backdrop-saturate":t=>({"--tw-backdrop-saturate":`saturate(${t})`,"@defaults backdrop-filter":{},"-webkit-backdrop-filter":ge,"backdrop-filter":ge})},{values:e("backdropSaturate")})},backdropSepia:({matchUtilities:r,theme:e})=>{r({"backdrop-sepia":t=>({"--tw-backdrop-sepia":`sepia(${t})`,"@defaults backdrop-filter":{},"-webkit-backdrop-filter":ge,"backdrop-filter":ge})},{values:e("backdropSepia")})},backdropFilter:({addDefaults:r,addUtilities:e})=>{r("backdrop-filter",{"--tw-backdrop-blur":" ","--tw-backdrop-brightness":" ","--tw-backdrop-contrast":" ","--tw-backdrop-grayscale":" ","--tw-backdrop-hue-rotate":" ","--tw-backdrop-invert":" ","--tw-backdrop-opacity":" ","--tw-backdrop-saturate":" ","--tw-backdrop-sepia":" "}),e({".backdrop-filter":{"@defaults backdrop-filter":{},"-webkit-backdrop-filter":ge,"backdrop-filter":ge},".backdrop-filter-none":{"-webkit-backdrop-filter":"none","backdrop-filter":"none"}})},transitionProperty:({matchUtilities:r,theme:e})=>{let t=e("transitionTimingFunction.DEFAULT"),i=e("transitionDuration.DEFAULT");r({transition:n=>({"transition-property":n,...n==="none"?{}:{"transition-timing-function":t,"transition-duration":i}})},{values:e("transitionProperty")})},transitionDelay:L("transitionDelay",[["delay",["transitionDelay"]]]),transitionDuration:L("transitionDuration",[["duration",["transitionDuration"]]],{filterDefault:!0}),transitionTimingFunction:L("transitionTimingFunction",[["ease",["transitionTimingFunction"]]],{filterDefault:!0}),willChange:L("willChange",[["will-change",["will-change"]]]),contain:({addDefaults:r,addUtilities:e})=>{let t="var(--tw-contain-size) var(--tw-contain-layout) var(--tw-contain-paint) var(--tw-contain-style)";r("contain",{"--tw-contain-size":" ","--tw-contain-layout":" ","--tw-contain-paint":" ","--tw-contain-style":" "}),e({".contain-none":{contain:"none"},".contain-content":{contain:"content"},".contain-strict":{contain:"strict"},".contain-size":{"@defaults contain":{},"--tw-contain-size":"size",contain:t},".contain-inline-size":{"@defaults contain":{},"--tw-contain-size":"inline-size",contain:t},".contain-layout":{"@defaults contain":{},"--tw-contain-layout":"layout",contain:t},".contain-paint":{"@defaults contain":{},"--tw-contain-paint":"paint",contain:t},".contain-style":{"@defaults contain":{},"--tw-contain-style":"style",contain:t}})},content:L("content",[["content",["--tw-content",["content","var(--tw-content)"]]]]),forcedColorAdjust:({addUtilities:r})=>{r({".forced-color-adjust-auto":{"forced-color-adjust":"auto"},".forced-color-adjust-none":{"forced-color-adjust":"none"}})}}});function h_(r){if(r===void 0)return!1;if(r==="true"||r==="1")return!0;if(r==="false"||r==="0")return!1;if(r==="*")return!0;let e=r.split(",").map(t=>t.split(":")[0]);return e.includes("-tailwindcss")?!1:!!e.includes("tailwindcss")}var Ze,wh,vh,es,No,gt,Ti,It=P(()=>{u();Ze=typeof m!="undefined"?{NODE_ENV:"production",DEBUG:h_(m.env.DEBUG)}:{NODE_ENV:"production",DEBUG:!1},wh=new Map,vh=new Map,es=new Map,No=new Map,gt=new String("*"),Ti=Symbol("__NONE__")});function cr(r){let e=[],t=!1;for(let i=0;i0)}var xh,kh,m_,Bo=P(()=>{u();xh=new Map([["{","}"],["[","]"],["(",")"]]),kh=new Map(Array.from(xh.entries()).map(([r,e])=>[e,r])),m_=new Set(['"',"'","`"])});function pr(r){let[e]=Sh(r);return e.forEach(([t,i])=>t.removeChild(i)),r.nodes.push(...e.map(([,t])=>t)),r}function Sh(r){let e=[],t=null;for(let i of r.nodes)if(i.type==="combinator")e=e.filter(([,n])=>jo(n).includes("jumpable")),t=null;else if(i.type==="pseudo"){g_(i)?(t=i,e.push([r,i,null])):t&&y_(i,t)?e.push([r,i,t]):t=null;for(let n of i.nodes??[]){let[s,a]=Sh(n);t=a||t,e.push(...s)}}return[e,t]}function Ah(r){return r.value.startsWith("::")||Fo[r.value]!==void 0}function g_(r){return Ah(r)&&jo(r).includes("terminal")}function y_(r,e){return r.type!=="pseudo"||Ah(r)?!1:jo(e).includes("actionable")}function jo(r){return Fo[r.value]??Fo.__default__}var Fo,ts=P(()=>{u();Fo={"::after":["terminal","jumpable"],"::backdrop":["terminal","jumpable"],"::before":["terminal","jumpable"],"::cue":["terminal"],"::cue-region":["terminal"],"::first-letter":["terminal","jumpable"],"::first-line":["terminal","jumpable"],"::grammar-error":["terminal"],"::marker":["terminal","jumpable"],"::part":["terminal","actionable"],"::placeholder":["terminal","jumpable"],"::selection":["terminal","jumpable"],"::slotted":["terminal"],"::spelling-error":["terminal"],"::target-text":["terminal"],"::file-selector-button":["terminal","actionable"],"::deep":["actionable"],"::v-deep":["actionable"],"::ng-deep":["actionable"],":after":["terminal","jumpable"],":before":["terminal","jumpable"],":first-letter":["terminal","jumpable"],":first-line":["terminal","jumpable"],":where":[],":is":[],":has":[],__default__:["terminal","actionable"]}});function dr(r,{context:e,candidate:t}){let i=e?.tailwindConfig.prefix??"",n=r.map(a=>{let o=(0,st.default)().astSync(a.format);return{...a,ast:a.respectPrefix?ur(i,o):o}}),s=st.default.root({nodes:[st.default.selector({nodes:[st.default.className({value:Te(t)})]})]});for(let{ast:a}of n)[s,a]=w_(s,a),a.walkNesting(o=>o.replaceWith(...s.nodes[0].nodes)),s=a;return s}function _h(r){let e=[];for(;r.prev()&&r.prev().type!=="combinator";)r=r.prev();for(;r&&r.type!=="combinator";)e.push(r),r=r.next();return e}function b_(r){return r.sort((e,t)=>e.type==="tag"&&t.type==="class"?-1:e.type==="class"&&t.type==="tag"?1:e.type==="class"&&t.type==="pseudo"&&t.value.startsWith("::")?-1:e.type==="pseudo"&&e.value.startsWith("::")&&t.type==="class"?1:r.index(e)-r.index(t)),r}function Uo(r,e){let t=!1;r.walk(i=>{if(i.type==="class"&&i.value===e)return t=!0,!1}),t||r.remove()}function rs(r,e,{context:t,candidate:i,base:n}){let s=t?.tailwindConfig?.separator??":";n=n??ve(i,s).pop();let a=(0,st.default)().astSync(r);if(a.walkClasses(f=>{f.raws&&f.value.includes(n)&&(f.raws.value=Te((0,Ch.default)(f.raws.value)))}),a.each(f=>Uo(f,n)),a.length===0)return null;let o=Array.isArray(e)?dr(e,{context:t,candidate:i}):e;if(o===null)return a.toString();let l=st.default.comment({value:"/*__simple__*/"}),c=st.default.comment({value:"/*__simple__*/"});return a.walkClasses(f=>{if(f.value!==n)return;let d=f.parent,p=o.nodes[0].nodes;if(d.nodes.length===1){f.replaceWith(...p);return}let h=_h(f);d.insertBefore(h[0],l),d.insertAfter(h[h.length-1],c);for(let v of p)d.insertBefore(h[0],v.clone());f.remove(),h=_h(l);let b=d.index(l);d.nodes.splice(b,h.length,...b_(st.default.selector({nodes:h})).nodes),l.remove(),c.remove()}),a.walkPseudos(f=>{f.value===zo&&f.replaceWith(f.nodes)}),a.each(f=>pr(f)),a.toString()}function w_(r,e){let t=[];return r.walkPseudos(i=>{i.value===zo&&t.push({pseudo:i,value:i.nodes[0].toString()})}),e.walkPseudos(i=>{if(i.value!==zo)return;let n=i.nodes[0].toString(),s=t.find(c=>c.value===n);if(!s)return;let a=[],o=i.next();for(;o&&o.type!=="combinator";)a.push(o),o=o.next();let l=o;s.pseudo.parent.insertAfter(s.pseudo,st.default.selector({nodes:a.map(c=>c.clone())})),i.remove(),a.forEach(c=>c.remove()),l&&l.type==="combinator"&&l.remove()}),[r,e]}var st,Ch,zo,Vo=P(()=>{u();st=pe(it()),Ch=pe(Pn());fr();Gn();ts();zt();zo=":merge"});function is(r,e){let t=(0,Ho.default)().astSync(r);return t.each(i=>{i.nodes.some(s=>s.type==="combinator")&&(i.nodes=[Ho.default.pseudo({value:":is",nodes:[i.clone()]})]),pr(i)}),`${e} ${t.toString()}`}var Ho,Wo=P(()=>{u();Ho=pe(it());ts()});function Go(r){return v_.transformSync(r)}function*x_(r){let e=1/0;for(;e>=0;){let t,i=!1;if(e===1/0&&r.endsWith("]")){let a=r.indexOf("[");r[a-1]==="-"?t=a-1:r[a-1]==="/"?(t=a-1,i=!0):t=-1}else e===1/0&&r.includes("/")?(t=r.lastIndexOf("/"),i=!0):t=r.lastIndexOf("-",e);if(t<0)break;let n=r.slice(0,t),s=r.slice(i?t:t+1);e=t-1,!(n===""||s==="/")&&(yield[n,s])}}function k_(r,e){if(r.length===0||e.tailwindConfig.prefix==="")return r;for(let t of r){let[i]=t;if(i.options.respectPrefix){let n=ee.root({nodes:[t[1].clone()]}),s=t[1].raws.tailwind.classCandidate;n.walkRules(a=>{let o=s.startsWith("-");a.selector=ur(e.tailwindConfig.prefix,a.selector,o)}),t[1]=n.nodes[0]}}return r}function S_(r,e){if(r.length===0)return r;let t=[];function i(n){return n.parent&&n.parent.type==="atrule"&&n.parent.name==="keyframes"}for(let[n,s]of r){let a=ee.root({nodes:[s.clone()]});a.walkRules(o=>{if(i(o))return;let l=(0,ns.default)().astSync(o.selector);l.each(c=>Uo(c,e)),Qf(l,c=>c===e?`!${c}`:c),o.selector=l.toString(),o.walkDecls(c=>c.important=!0)}),t.push([{...n,important:!0},a.nodes[0]])}return t}function A_(r,e,t){if(e.length===0)return e;let i={modifier:null,value:Ti};{let[n,...s]=ve(r,"/");if(s.length>1&&(n=n+"/"+s.slice(0,-1).join("/"),s=s.slice(-1)),s.length&&!t.variantMap.has(r)&&(r=n,i.modifier=s[0],!we(t.tailwindConfig,"generalizedModifiers")))return[]}if(r.endsWith("]")&&!r.startsWith("[")){let n=/(.)(-?)\[(.*)\]/g.exec(r);if(n){let[,s,a,o]=n;if(s==="@"&&a==="-")return[];if(s!=="@"&&a==="")return[];r=r.replace(`${a}[${o}]`,""),i.value=o}}if(Ko(r)&&!t.variantMap.has(r)){let n=t.offsets.recordVariant(r),s=K(r.slice(1,-1)),a=ve(s,",");if(a.length>1)return[];if(!a.every(ls))return[];let o=a.map((l,c)=>[t.offsets.applyParallelOffset(n,c),Ri(l.trim())]);t.variantMap.set(r,o)}if(t.variantMap.has(r)){let n=Ko(r),s=t.variantOptions.get(r)?.[Pt]??{},a=t.variantMap.get(r).slice(),o=[],l=(()=>!(n||s.respectPrefix===!1))();for(let[c,f]of e){if(c.layer==="user")continue;let d=ee.root({nodes:[f.clone()]});for(let[p,h,b]of a){let w=function(){v.raws.neededBackup||(v.raws.neededBackup=!0,v.walkRules(T=>T.raws.originalSelector=T.selector))},k=function(T){return w(),v.each(B=>{B.type==="rule"&&(B.selectors=B.selectors.map(N=>T({get className(){return Go(N)},selector:N})))}),v},v=(b??d).clone(),y=[],S=h({get container(){return w(),v},separator:t.tailwindConfig.separator,modifySelectors:k,wrap(T){let B=v.nodes;v.removeAll(),T.append(B),v.append(T)},format(T){y.push({format:T,respectPrefix:l})},args:i});if(Array.isArray(S)){for(let[T,B]of S.entries())a.push([t.offsets.applyParallelOffset(p,T),B,v.clone()]);continue}if(typeof S=="string"&&y.push({format:S,respectPrefix:l}),S===null)continue;v.raws.neededBackup&&(delete v.raws.neededBackup,v.walkRules(T=>{let B=T.raws.originalSelector;if(!B||(delete T.raws.originalSelector,B===T.selector))return;let N=T.selector,R=(0,ns.default)(F=>{F.walkClasses(Y=>{Y.value=`${r}${t.tailwindConfig.separator}${Y.value}`})}).processSync(B);y.push({format:N.replace(R,"&"),respectPrefix:l}),T.selector=B})),v.nodes[0].raws.tailwind={...v.nodes[0].raws.tailwind,parentLayer:c.layer};let E=[{...c,sort:t.offsets.applyVariantOffset(c.sort,p,Object.assign(i,t.variantOptions.get(r))),collectedFormats:(c.collectedFormats??[]).concat(y)},v.nodes[0]];o.push(E)}}return o}return[]}function Qo(r,e,t={}){return!ke(r)&&!Array.isArray(r)?[[r],t]:Array.isArray(r)?Qo(r[0],e,r[1]):(e.has(r)||e.set(r,lr(r)),[e.get(r),t])}function __(r){return C_.test(r)}function E_(r){if(!r.includes("://"))return!1;try{let e=new URL(r);return e.scheme!==""&&e.host!==""}catch(e){return!1}}function Eh(r){let e=!0;return r.walkDecls(t=>{if(!Oh(t.prop,t.value))return e=!1,!1}),e}function Oh(r,e){if(E_(`${r}:${e}`))return!1;try{return ee.parse(`a{${r}:${e}}`).toResult(),!0}catch(t){return!1}}function O_(r,e){let[,t,i]=r.match(/^\[([a-zA-Z0-9-_]+):(\S+)\]$/)??[];if(i===void 0||!__(t)||!cr(i))return null;let n=K(i,{property:t});return Oh(t,n)?[[{sort:e.offsets.arbitraryProperty(r),layer:"utilities",options:{respectImportant:!0}},()=>({[$o(r)]:{[t]:n}})]]:null}function*T_(r,e){e.candidateRuleMap.has(r)&&(yield[e.candidateRuleMap.get(r),"DEFAULT"]),yield*function*(o){o!==null&&(yield[o,"DEFAULT"])}(O_(r,e));let t=r,i=!1,n=e.tailwindConfig.prefix,s=n.length,a=t.startsWith(n)||t.startsWith(`-${n}`);t[s]==="-"&&a&&(i=!0,t=n+t.slice(s+1)),i&&e.candidateRuleMap.has(t)&&(yield[e.candidateRuleMap.get(t),"-DEFAULT"]);for(let[o,l]of x_(t))e.candidateRuleMap.has(o)&&(yield[e.candidateRuleMap.get(o),i?`-${l}`:l])}function R_(r,e){return r===gt?[gt]:ve(r,e)}function*P_(r,e){for(let t of r)t[1].raws.tailwind={...t[1].raws.tailwind,classCandidate:e,preserveSource:t[0].options?.preserveSource??!1},yield t}function*Yo(r,e){let t=e.tailwindConfig.separator,[i,...n]=R_(r,t).reverse(),s=!1;i.startsWith("!")&&(s=!0,i=i.slice(1));for(let a of T_(i,e)){let o=[],l=new Map,[c,f]=a,d=c.length===1;for(let[p,h]of c){let b=[];if(typeof h=="function")for(let v of[].concat(h(f,{isOnlyPlugin:d}))){let[y,w]=Qo(v,e.postCssNodeCache);for(let k of y)b.push([{...p,options:{...p.options,...w}},k])}else if(f==="DEFAULT"||f==="-DEFAULT"){let v=h,[y,w]=Qo(v,e.postCssNodeCache);for(let k of y)b.push([{...p,options:{...p.options,...w}},k])}if(b.length>0){let v=Array.from(ta(p.options?.types??[],f,p.options??{},e.tailwindConfig)).map(([y,w])=>w);v.length>0&&l.set(b,v),o.push(b)}}if(Ko(f)){if(o.length>1){let b=function(y){return y.length===1?y[0]:y.find(w=>{let k=l.get(w);return w.some(([{options:S},E])=>Eh(E)?S.types.some(({type:T,preferOnConflict:B})=>k.includes(T)&&B):!1)})},[p,h]=o.reduce((y,w)=>(w.some(([{options:S}])=>S.types.some(({type:E})=>E==="any"))?y[0].push(w):y[1].push(w),y),[[],[]]),v=b(h)??b(p);if(v)o=[v];else{let y=o.map(k=>new Set([...l.get(k)??[]]));for(let k of y)for(let S of k){let E=!1;for(let T of y)k!==T&&T.has(S)&&(T.delete(S),E=!0);E&&k.delete(S)}let w=[];for(let[k,S]of y.entries())for(let E of S){let T=o[k].map(([,B])=>B).flat().map(B=>B.toString().split(` +`).slice(1,-1).map(N=>N.trim()).map(N=>` ${N}`).join(` +`)).join(` + +`);w.push(` Use \`${r.replace("[",`[${E}:`)}\` for \`${T.trim()}\``);break}G.warn([`The class \`${r}\` is ambiguous and matches multiple utilities.`,...w,`If this is content and not a class, replace it with \`${r.replace("[","[").replace("]","]")}\` to silence this warning.`]);continue}}o=o.map(p=>p.filter(h=>Eh(h[1])))}o=o.flat(),o=Array.from(P_(o,i)),o=k_(o,e),s&&(o=S_(o,i));for(let p of n)o=A_(p,o,e);for(let p of o)p[1].raws.tailwind={...p[1].raws.tailwind,candidate:r},p=I_(p,{context:e,candidate:r}),p!==null&&(yield p)}}function I_(r,{context:e,candidate:t}){if(!r[0].collectedFormats)return r;let i=!0,n;try{n=dr(r[0].collectedFormats,{context:e,candidate:t})}catch{return null}let s=ee.root({nodes:[r[1].clone()]});return s.walkRules(a=>{if(!ss(a))try{let o=rs(a.selector,n,{candidate:t,context:e});if(o===null){a.remove();return}a.selector=o}catch{return i=!1,!1}}),!i||s.nodes.length===0?null:(r[1]=s.nodes[0],r)}function ss(r){return r.parent&&r.parent.type==="atrule"&&r.parent.name==="keyframes"}function D_(r){if(r===!0)return e=>{ss(e)||e.walkDecls(t=>{t.parent.type==="rule"&&!ss(t.parent)&&(t.important=!0)})};if(typeof r=="string")return e=>{ss(e)||(e.selectors=e.selectors.map(t=>is(t,r)))}}function as(r,e,t=!1){let i=[],n=D_(e.tailwindConfig.important);for(let s of r){if(e.notClassCache.has(s))continue;if(e.candidateRuleCache.has(s)){i=i.concat(Array.from(e.candidateRuleCache.get(s)));continue}let a=Array.from(Yo(s,e));if(a.length===0){e.notClassCache.add(s);continue}e.classCache.set(s,a);let o=e.candidateRuleCache.get(s)??new Set;e.candidateRuleCache.set(s,o);for(let l of a){let[{sort:c,options:f},d]=l;if(f.respectImportant&&n){let h=ee.root({nodes:[d.clone()]});h.walkRules(n),d=h.nodes[0]}let p=[c,t?d.clone():d];o.add(p),e.ruleCache.add(p),i.push(p)}}return i}function Ko(r){return r.startsWith("[")&&r.endsWith("]")}var ns,v_,C_,os=P(()=>{u();Ot();ns=pe(it());qo();Kt();Gn();Fr();Be();It();Vo();Lo();Br();Oi();Bo();zt();ct();Wo();v_=(0,ns.default)(r=>r.first.filter(({type:e})=>e==="class").pop().value);C_=/^[a-z_-]/});var Th,Rh=P(()=>{u();Th={}});function q_(r){try{return Th.createHash("md5").update(r,"utf-8").digest("binary")}catch(e){return""}}function Ph(r,e){let t=e.toString();if(!t.includes("@tailwind"))return!1;let i=No.get(r),n=q_(t),s=i!==n;return No.set(r,n),s}var Ih=P(()=>{u();Rh();It()});function us(r){return(r>0n)-(r<0n)}var Dh=P(()=>{u()});function qh(r,e){let t=0n,i=0n;for(let[n,s]of e)r&n&&(t=t|n,i=i|s);return r&~t|i}var $h=P(()=>{u()});function Lh(r){let e=null;for(let t of r)e=e??t,e=e>t?e:t;return e}function $_(r,e){let t=r.length,i=e.length,n=t{u();Dh();$h();Xo=class{constructor(){this.offsets={defaults:0n,base:0n,components:0n,utilities:0n,variants:0n,user:0n},this.layerPositions={defaults:0n,base:1n,components:2n,utilities:3n,user:4n,variants:5n},this.reservedVariantBits=0n,this.variantOffsets=new Map}create(e){return{layer:e,parentLayer:e,arbitrary:0n,variants:0n,parallelIndex:0n,index:this.offsets[e]++,propertyOffset:0n,property:"",options:[]}}arbitraryProperty(e){return{...this.create("utilities"),arbitrary:1n,property:e}}forVariant(e,t=0){let i=this.variantOffsets.get(e);if(i===void 0)throw new Error(`Cannot find offset for unknown variant ${e}`);return{...this.create("variants"),variants:i<n.startsWith("[")).sort(([n],[s])=>$_(n,s)),t=e.map(([,n])=>n).sort((n,s)=>us(n-s));return e.map(([,n],s)=>[n,t[s]]).filter(([n,s])=>n!==s)}remapArbitraryVariantOffsets(e){let t=this.recalculateVariantOffsets();return t.length===0?e:e.map(i=>{let[n,s]=i;return n={...n,variants:qh(n.variants,t)},[n,s]})}sortArbitraryProperties(e){let t=new Set;for(let[a]of e)a.arbitrary===1n&&t.add(a.property);if(t.size===0)return e;let i=Array.from(t).sort(),n=new Map,s=1n;for(let a of i)n.set(a,s++);return e.map(a=>{let[o,l]=a;return o={...o,propertyOffset:n.get(o.property)??0n},[o,l]})}sort(e){return e=this.remapArbitraryVariantOffsets(e),e=this.sortArbitraryProperties(e),e.sort(([t],[i])=>us(this.compare(t,i)))}}});function tl(r,e){let t=r.tailwindConfig.prefix;return typeof t=="function"?t(e):t+e}function Bh({type:r="any",...e}){let t=[].concat(r);return{...e,types:t.map(i=>Array.isArray(i)?{type:i[0],...i[1]}:{type:i,preferOnConflict:!1})}}function L_(r){let e=[],t="",i=0;for(let n=0;n0&&e.push(t.trim()),e=e.filter(n=>n!==""),e}function M_(r,e,{before:t=[]}={}){if(t=[].concat(t),t.length<=0){r.push(e);return}let i=r.length-1;for(let n of t){let s=r.indexOf(n);s!==-1&&(i=Math.min(i,s))}r.splice(i,0,e)}function Fh(r){return Array.isArray(r)?r.flatMap(e=>!Array.isArray(e)&&!ke(e)?e:lr(e)):Fh([r])}function N_(r,e){return(0,Zo.default)(i=>{let n=[];return e&&e(i),i.walkClasses(s=>{n.push(s.value)}),n}).transformSync(r)}function B_(r){r.walkPseudos(e=>{e.value===":not"&&e.remove()})}function F_(r,e={containsNonOnDemandable:!1},t=0){let i=[],n=[];r.type==="rule"?n.push(...r.selectors):r.type==="atrule"&&r.walkRules(s=>n.push(...s.selectors));for(let s of n){let a=N_(s,B_);a.length===0&&(e.containsNonOnDemandable=!0);for(let o of a)i.push(o)}return t===0?[e.containsNonOnDemandable||i.length===0,i]:i}function fs(r){return Fh(r).flatMap(e=>{let t=new Map,[i,n]=F_(e);return i&&n.unshift(gt),n.map(s=>(t.has(e)||t.set(e,e),[s,t.get(e)]))})}function ls(r){return r.startsWith("@")||r.includes("&")}function Ri(r){r=r.replace(/\n+/g,"").replace(/\s{1,}/g," ").trim();let e=L_(r).map(t=>{if(!t.startsWith("@"))return({format:s})=>s(t);let[,i,n]=/@(\S*)( .+|[({].*)?/g.exec(t);return({wrap:s})=>s(ee.atRule({name:i,params:n?.trim()??""}))}).reverse();return t=>{for(let i of e)i(t)}}function j_(r,e,{variantList:t,variantMap:i,offsets:n,classList:s}){function a(p,h){return p?(0,Nh.default)(r,p,h):r}function o(p){return ur(r.prefix,p)}function l(p,h){return p===gt?gt:h.respectPrefix?e.tailwindConfig.prefix+p:p}function c(p,h,b={}){let v=kt(p),y=a(["theme",...v],h);return mt(v[0])(y,b)}let f=0,d={postcss:ee,prefix:o,e:Te,config:a,theme:c,corePlugins:p=>Array.isArray(r.corePlugins)?r.corePlugins.includes(p):a(["corePlugins",p],!0),variants:()=>[],addBase(p){for(let[h,b]of fs(p)){let v=l(h,{}),y=n.create("base");e.candidateRuleMap.has(v)||e.candidateRuleMap.set(v,[]),e.candidateRuleMap.get(v).push([{sort:y,layer:"base"},b])}},addDefaults(p,h){let b={[`@defaults ${p}`]:h};for(let[v,y]of fs(b)){let w=l(v,{});e.candidateRuleMap.has(w)||e.candidateRuleMap.set(w,[]),e.candidateRuleMap.get(w).push([{sort:n.create("defaults"),layer:"defaults"},y])}},addComponents(p,h){h=Object.assign({},{preserveSource:!1,respectPrefix:!0,respectImportant:!1},Array.isArray(h)?{}:h);for(let[v,y]of fs(p)){let w=l(v,h);s.add(w),e.candidateRuleMap.has(w)||e.candidateRuleMap.set(w,[]),e.candidateRuleMap.get(w).push([{sort:n.create("components"),layer:"components",options:h},y])}},addUtilities(p,h){h=Object.assign({},{preserveSource:!1,respectPrefix:!0,respectImportant:!0},Array.isArray(h)?{}:h);for(let[v,y]of fs(p)){let w=l(v,h);s.add(w),e.candidateRuleMap.has(w)||e.candidateRuleMap.set(w,[]),e.candidateRuleMap.get(w).push([{sort:n.create("utilities"),layer:"utilities",options:h},y])}},matchUtilities:function(p,h){h=Bh({...{respectPrefix:!0,respectImportant:!0,modifiers:!1},...h});let v=n.create("utilities");for(let y in p){let S=function(T,{isOnlyPlugin:B}){let[N,R,F]=ea(h.types,T,h,r);if(N===void 0)return[];if(!h.types.some(({type:U})=>U===R))if(B)G.warn([`Unnecessary typehint \`${R}\` in \`${y}-${T}\`.`,`You can safely update it to \`${y}-${T.replace(R+":","")}\`.`]);else return[];if(!cr(N))return[];let Y={get modifier(){return h.modifiers||G.warn(`modifier-used-without-options-for-${y}`,["Your plugin must set `modifiers: true` in its options to support modifiers."]),F}},_=we(r,"generalizedModifiers");return[].concat(_?k(N,Y):k(N)).filter(Boolean).map(U=>({[Qn(y,T)]:U}))},w=l(y,h),k=p[y];s.add([w,h]);let E=[{sort:v,layer:"utilities",options:h},S];e.candidateRuleMap.has(w)||e.candidateRuleMap.set(w,[]),e.candidateRuleMap.get(w).push(E)}},matchComponents:function(p,h){h=Bh({...{respectPrefix:!0,respectImportant:!1,modifiers:!1},...h});let v=n.create("components");for(let y in p){let S=function(T,{isOnlyPlugin:B}){let[N,R,F]=ea(h.types,T,h,r);if(N===void 0)return[];if(!h.types.some(({type:U})=>U===R))if(B)G.warn([`Unnecessary typehint \`${R}\` in \`${y}-${T}\`.`,`You can safely update it to \`${y}-${T.replace(R+":","")}\`.`]);else return[];if(!cr(N))return[];let Y={get modifier(){return h.modifiers||G.warn(`modifier-used-without-options-for-${y}`,["Your plugin must set `modifiers: true` in its options to support modifiers."]),F}},_=we(r,"generalizedModifiers");return[].concat(_?k(N,Y):k(N)).filter(Boolean).map(U=>({[Qn(y,T)]:U}))},w=l(y,h),k=p[y];s.add([w,h]);let E=[{sort:v,layer:"components",options:h},S];e.candidateRuleMap.has(w)||e.candidateRuleMap.set(w,[]),e.candidateRuleMap.get(w).push(E)}},addVariant(p,h,b={}){h=[].concat(h).map(v=>{if(typeof v!="string")return(y={})=>{let{args:w,modifySelectors:k,container:S,separator:E,wrap:T,format:B}=y,N=v(Object.assign({modifySelectors:k,container:S,separator:E},b.type===Jo.MatchVariant&&{args:w,wrap:T,format:B}));if(typeof N=="string"&&!ls(N))throw new Error(`Your custom variant \`${p}\` has an invalid format string. Make sure it's an at-rule or contains a \`&\` placeholder.`);return Array.isArray(N)?N.filter(R=>typeof R=="string").map(R=>Ri(R)):N&&typeof N=="string"&&Ri(N)(y)};if(!ls(v))throw new Error(`Your custom variant \`${p}\` has an invalid format string. Make sure it's an at-rule or contains a \`&\` placeholder.`);return Ri(v)}),M_(t,p,b),i.set(p,h),e.variantOptions.set(p,b)},matchVariant(p,h,b){let v=b?.id??++f,y=p==="@",w=we(r,"generalizedModifiers");for(let[S,E]of Object.entries(b?.values??{}))S!=="DEFAULT"&&d.addVariant(y?`${p}${S}`:`${p}-${S}`,({args:T,container:B})=>h(E,w?{modifier:T?.modifier,container:B}:{container:B}),{...b,value:E,id:v,type:Jo.MatchVariant,variantInfo:el.Base});let k="DEFAULT"in(b?.values??{});d.addVariant(p,({args:S,container:E})=>S?.value===Ti&&!k?null:h(S?.value===Ti?b.values.DEFAULT:S?.value??(typeof S=="string"?S:""),w?{modifier:S?.modifier,container:E}:{container:E}),{...b,id:v,type:Jo.MatchVariant,variantInfo:el.Dynamic})}};return d}function cs(r){return rl.has(r)||rl.set(r,new Map),rl.get(r)}function jh(r,e){let t=!1,i=new Map;for(let n of r){if(!n)continue;let s=oa.parse(n),a=s.hash?s.href.replace(s.hash,""):s.href;a=s.search?a.replace(s.search,""):a;let o=be.statSync(decodeURIComponent(a),{throwIfNoEntry:!1})?.mtimeMs;!o||((!e.has(n)||o>e.get(n))&&(t=!0),i.set(n,o))}return[t,i]}function zh(r){r.walkAtRules(e=>{["responsive","variants"].includes(e.name)&&(zh(e),e.before(e.nodes),e.remove())})}function z_(r){let e=[];return r.each(t=>{t.type==="atrule"&&["responsive","variants"].includes(t.name)&&(t.name="layer",t.params="utilities")}),r.walkAtRules("layer",t=>{if(zh(t),t.params==="base"){for(let i of t.nodes)e.push(function({addBase:n}){n(i,{respectPrefix:!1})});t.remove()}else if(t.params==="components"){for(let i of t.nodes)e.push(function({addComponents:n}){n(i,{respectPrefix:!1,preserveSource:!0})});t.remove()}else if(t.params==="utilities"){for(let i of t.nodes)e.push(function({addUtilities:n}){n(i,{respectPrefix:!1,preserveSource:!0})});t.remove()}}),e}function U_(r,e){let t=Object.entries({...se,...yh}).map(([l,c])=>r.tailwindConfig.corePlugins.includes(l)?c:null).filter(Boolean),i=r.tailwindConfig.plugins.map(l=>(l.__isOptionsFunction&&(l=l()),typeof l=="function"?l:l.handler)),n=z_(e),s=[se.childVariant,se.pseudoElementVariants,se.pseudoClassVariants,se.hasVariants,se.ariaVariants,se.dataVariants],a=[se.supportsVariants,se.reducedMotionVariants,se.prefersContrastVariants,se.screenVariants,se.orientationVariants,se.directionVariants,se.darkVariants,se.forcedColorsVariants,se.printVariant];return(r.tailwindConfig.darkMode==="class"||Array.isArray(r.tailwindConfig.darkMode)&&r.tailwindConfig.darkMode[0]==="class")&&(a=[se.supportsVariants,se.reducedMotionVariants,se.prefersContrastVariants,se.darkVariants,se.screenVariants,se.orientationVariants,se.directionVariants,se.forcedColorsVariants,se.printVariant]),[...t,...s,...i,...a,...n]}function V_(r,e){let t=[],i=new Map;e.variantMap=i;let n=new Xo;e.offsets=n;let s=new Set,a=j_(e.tailwindConfig,e,{variantList:t,variantMap:i,offsets:n,classList:s});for(let f of r)if(Array.isArray(f))for(let d of f)d(a);else f?.(a);n.recordVariants(t,f=>i.get(f).length);for(let[f,d]of i.entries())e.variantMap.set(f,d.map((p,h)=>[n.forVariant(f,h),p]));let o=(e.tailwindConfig.safelist??[]).filter(Boolean);if(o.length>0){let f=[];for(let d of o){if(typeof d=="string"){e.changedContent.push({content:d,extension:"html"});continue}if(d instanceof RegExp){G.warn("root-regex",["Regular expressions in `safelist` work differently in Tailwind CSS v3.0.","Update your `safelist` configuration to eliminate this warning.","https://tailwindcss.com/docs/content-configuration#safelisting-classes"]);continue}f.push(d)}if(f.length>0){let d=new Map,p=e.tailwindConfig.prefix.length,h=f.some(b=>b.pattern.source.includes("!"));for(let b of s){let v=Array.isArray(b)?(()=>{let[y,w]=b,S=Object.keys(w?.values??{}).map(E=>Ei(y,E));return w?.supportsNegativeValues&&(S=[...S,...S.map(E=>"-"+E)],S=[...S,...S.map(E=>E.slice(0,p)+"-"+E.slice(p))]),w.types.some(({type:E})=>E==="color")&&(S=[...S,...S.flatMap(E=>Object.keys(e.tailwindConfig.theme.opacity).map(T=>`${E}/${T}`))]),h&&w?.respectImportant&&(S=[...S,...S.map(E=>"!"+E)]),S})():[b];for(let y of v)for(let{pattern:w,variants:k=[]}of f)if(w.lastIndex=0,d.has(w)||d.set(w,0),!!w.test(y)){d.set(w,d.get(w)+1),e.changedContent.push({content:y,extension:"html"});for(let S of k)e.changedContent.push({content:S+e.tailwindConfig.separator+y,extension:"html"})}}for(let[b,v]of d.entries())v===0&&G.warn([`The safelist pattern \`${b}\` doesn't match any Tailwind CSS classes.`,"Fix this pattern or remove it from your `safelist` configuration.","https://tailwindcss.com/docs/content-configuration#safelisting-classes"])}}let l=[].concat(e.tailwindConfig.darkMode??"media")[1]??"dark",c=[tl(e,l),tl(e,"group"),tl(e,"peer")];e.getClassOrder=function(d){let p=[...d].sort((y,w)=>y===w?0:y[y,null])),b=as(new Set(p),e,!0);b=e.offsets.sort(b);let v=BigInt(c.length);for(let[,y]of b){let w=y.raws.tailwind.candidate;h.set(w,h.get(w)??v++)}return d.map(y=>{let w=h.get(y)??null,k=c.indexOf(y);return w===null&&k!==-1&&(w=BigInt(k)),[y,w]})},e.getClassList=function(d={}){let p=[];for(let h of s)if(Array.isArray(h)){let[b,v]=h,y=[],w=Object.keys(v?.modifiers??{});v?.types?.some(({type:E})=>E==="color")&&w.push(...Object.keys(e.tailwindConfig.theme.opacity??{}));let k={modifiers:w},S=d.includeMetadata&&w.length>0;for(let[E,T]of Object.entries(v?.values??{})){if(T==null)continue;let B=Ei(b,E);if(p.push(S?[B,k]:B),v?.supportsNegativeValues&&xt(T)){let N=Ei(b,`-${E}`);y.push(S?[N,k]:N)}}p.push(...y)}else p.push(h);return p},e.getVariants=function(){let d=Math.random().toString(36).substring(7).toUpperCase(),p=[];for(let[h,b]of e.variantOptions.entries())b.variantInfo!==el.Base&&p.push({name:h,isArbitrary:b.type===Symbol.for("MATCH_VARIANT"),values:Object.keys(b.values??{}),hasDash:h!=="@",selectors({modifier:v,value:y}={}){let w=`TAILWINDPLACEHOLDER${d}`,k=ee.rule({selector:`.${w}`}),S=ee.root({nodes:[k.clone()]}),E=S.toString(),T=(e.variantMap.get(h)??[]).flatMap(([le,A])=>A),B=[];for(let le of T){let A=[],C={args:{modifier:v,value:b.values?.[y]??y},separator:e.tailwindConfig.separator,modifySelectors(V){return S.each(Ee=>{Ee.type==="rule"&&(Ee.selectors=Ee.selectors.map(Ie=>V({get className(){return Go(Ie)},selector:Ie})))}),S},format(V){A.push(V)},wrap(V){A.push(`@${V.name} ${V.params} { & }`)},container:S},he=le(C);if(A.length>0&&B.push(A),Array.isArray(he))for(let V of he)A=[],V(C),B.push(A)}let N=[],R=S.toString();E!==R&&(S.walkRules(le=>{let A=le.selector,C=(0,Zo.default)(he=>{he.walkClasses(V=>{V.value=`${h}${e.tailwindConfig.separator}${V.value}`})}).processSync(A);N.push(A.replace(C,"&").replace(w,"&"))}),S.walkAtRules(le=>{N.push(`@${le.name} (${le.params}) { & }`)}));let F=!(y in(b.values??{})),Y=b[Pt]??{},_=(()=>!(F||Y.respectPrefix===!1))();B=B.map(le=>le.map(A=>({format:A,respectPrefix:_}))),N=N.map(le=>({format:le,respectPrefix:_}));let Q={candidate:w,context:e},U=B.map(le=>rs(`.${w}`,dr(le,Q),Q).replace(`.${w}`,"&").replace("{ & }","").trim());return N.length>0&&U.push(dr(N,Q).toString().replace(`.${w}`,"&")),U}});return p}}function Uh(r,e){!r.classCache.has(e)||(r.notClassCache.add(e),r.classCache.delete(e),r.applyClassCache.delete(e),r.candidateRuleMap.delete(e),r.candidateRuleCache.delete(e),r.stylesheetCache=null)}function H_(r,e){let t=e.raws.tailwind.candidate;if(!!t){for(let i of r.ruleCache)i[1].raws.tailwind.candidate===t&&r.ruleCache.delete(i);Uh(r,t)}}function il(r,e=[],t=ee.root()){let i={disposables:[],ruleCache:new Set,candidateRuleCache:new Map,classCache:new Map,applyClassCache:new Map,notClassCache:new Set(r.blocklist??[]),postCssNodeCache:new Map,candidateRuleMap:new Map,tailwindConfig:r,changedContent:e,variantMap:new Map,stylesheetCache:null,variantOptions:new Map,markInvalidUtilityCandidate:s=>Uh(i,s),markInvalidUtilityNode:s=>H_(i,s)},n=U_(i,t);return V_(n,i),i}function Vh(r,e,t,i,n,s){let a=e.opts.from,o=i!==null;Ze.DEBUG&&console.log("Source path:",a);let l;if(o&&hr.has(a))l=hr.get(a);else if(Pi.has(n)){let p=Pi.get(n);Dt.get(p).add(a),hr.set(a,p),l=p}let c=Ph(a,r);if(l){let[p,h]=jh([...s],cs(l));if(!p&&!c)return[l,!1,h]}if(hr.has(a)){let p=hr.get(a);if(Dt.has(p)&&(Dt.get(p).delete(a),Dt.get(p).size===0)){Dt.delete(p);for(let[h,b]of Pi)b===p&&Pi.delete(h);for(let h of p.disposables.splice(0))h(p)}}Ze.DEBUG&&console.log("Setting up new context...");let f=il(t,[],r);Object.assign(f,{userConfigPath:i});let[,d]=jh([...s],cs(f));return Pi.set(n,f),hr.set(a,f),Dt.has(f)||Dt.set(f,new Set),Dt.get(f).add(a),[f,!0,d]}var Nh,Zo,Pt,Jo,el,rl,hr,Pi,Dt,Oi=P(()=>{u();ft();la();Ot();Nh=pe(Ra()),Zo=pe(it());Ci();qo();Gn();Kt();fr();Lo();Fr();bh();It();It();Yi();Be();Gi();Bo();os();Ih();Mh();ct();Vo();Pt=Symbol(),Jo={AddVariant:Symbol.for("ADD_VARIANT"),MatchVariant:Symbol.for("MATCH_VARIANT")},el={Base:1<<0,Dynamic:1<<1};rl=new WeakMap;hr=wh,Pi=vh,Dt=es});function nl(r){return r.ignore?[]:r.glob?m.env.ROLLUP_WATCH==="true"?[{type:"dependency",file:r.base}]:[{type:"dir-dependency",dir:r.base,glob:r.glob}]:[{type:"dependency",file:r.base}]}var Hh=P(()=>{u()});function Wh(r,e){return{handler:r,config:e}}var Gh,Qh=P(()=>{u();Wh.withOptions=function(r,e=()=>({})){let t=function(i){return{__options:i,handler:r(i),config:e(i)}};return t.__isOptionsFunction=!0,t.__pluginFunction=r,t.__configFunction=e,t};Gh=Wh});var sl={};Ge(sl,{default:()=>W_});var W_,al=P(()=>{u();Qh();W_=Gh});var Kh=x((z4,Yh)=>{u();var G_=(al(),sl).default,Q_={overflow:"hidden",display:"-webkit-box","-webkit-box-orient":"vertical"},Y_=G_(function({matchUtilities:r,addUtilities:e,theme:t,variants:i}){let n=t("lineClamp");r({"line-clamp":s=>({...Q_,"-webkit-line-clamp":`${s}`})},{values:n}),e([{".line-clamp-none":{"-webkit-line-clamp":"unset"}}],i("lineClamp"))},{theme:{lineClamp:{1:"1",2:"2",3:"3",4:"4",5:"5",6:"6"}},variants:{lineClamp:["responsive"]}});Yh.exports=Y_});function ol(r){r.content.files.length===0&&G.warn("content-problems",["The `content` option in your Tailwind CSS configuration is missing or empty.","Configure your content sources or your generated CSS will be missing styles.","https://tailwindcss.com/docs/content-configuration"]);try{let e=Kh();r.plugins.includes(e)&&(G.warn("line-clamp-in-core",["As of Tailwind CSS v3.3, the `@tailwindcss/line-clamp` plugin is now included by default.","Remove it from the `plugins` array in your configuration to eliminate this warning."]),r.plugins=r.plugins.filter(t=>t!==e))}catch{}return r}var Xh=P(()=>{u();Be()});var Zh,Jh=P(()=>{u();Zh=()=>!1});var ps,em=P(()=>{u();ps={sync:r=>[].concat(r),generateTasks:r=>[{dynamic:!1,base:".",negative:[],positive:[].concat(r),patterns:[].concat(r)}],escapePath:r=>r}});var ll,tm=P(()=>{u();ll=r=>r});var rm,im=P(()=>{u();rm=()=>""});function nm(r){let e=r,t=rm(r);return t!=="."&&(e=r.substr(t.length),e.charAt(0)==="/"&&(e=e.substr(1))),e.substr(0,2)==="./"?e=e.substr(2):e.charAt(0)==="/"&&(e=e.substr(1)),{base:t,glob:e}}var sm=P(()=>{u();im()});var ds=x(Ve=>{u();"use strict";Ve.isInteger=r=>typeof r=="number"?Number.isInteger(r):typeof r=="string"&&r.trim()!==""?Number.isInteger(Number(r)):!1;Ve.find=(r,e)=>r.nodes.find(t=>t.type===e);Ve.exceedsLimit=(r,e,t=1,i)=>i===!1||!Ve.isInteger(r)||!Ve.isInteger(e)?!1:(Number(e)-Number(r))/Number(t)>=i;Ve.escapeNode=(r,e=0,t)=>{let i=r.nodes[e];!i||(t&&i.type===t||i.type==="open"||i.type==="close")&&i.escaped!==!0&&(i.value="\\"+i.value,i.escaped=!0)};Ve.encloseBrace=r=>r.type!=="brace"?!1:r.commas>>0+r.ranges>>0==0?(r.invalid=!0,!0):!1;Ve.isInvalidBrace=r=>r.type!=="brace"?!1:r.invalid===!0||r.dollar?!0:r.commas>>0+r.ranges>>0==0||r.open!==!0||r.close!==!0?(r.invalid=!0,!0):!1;Ve.isOpenOrClose=r=>r.type==="open"||r.type==="close"?!0:r.open===!0||r.close===!0;Ve.reduce=r=>r.reduce((e,t)=>(t.type==="text"&&e.push(t.value),t.type==="range"&&(t.type="text"),e),[]);Ve.flatten=(...r)=>{let e=[],t=i=>{for(let n=0;n{u();"use strict";var am=ds();om.exports=(r,e={})=>{let t=(i,n={})=>{let s=e.escapeInvalid&&am.isInvalidBrace(n),a=i.invalid===!0&&e.escapeInvalid===!0,o="";if(i.value)return(s||a)&&am.isOpenOrClose(i)?"\\"+i.value:i.value;if(i.value)return i.value;if(i.nodes)for(let l of i.nodes)o+=t(l);return o};return t(r)}});var um=x((J4,lm)=>{u();"use strict";lm.exports=function(r){return typeof r=="number"?r-r==0:typeof r=="string"&&r.trim()!==""?Number.isFinite?Number.isFinite(+r):isFinite(+r):!1}});var bm=x((e6,ym)=>{u();"use strict";var fm=um(),Wt=(r,e,t)=>{if(fm(r)===!1)throw new TypeError("toRegexRange: expected the first argument to be a number");if(e===void 0||r===e)return String(r);if(fm(e)===!1)throw new TypeError("toRegexRange: expected the second argument to be a number.");let i={relaxZeros:!0,...t};typeof i.strictZeros=="boolean"&&(i.relaxZeros=i.strictZeros===!1);let n=String(i.relaxZeros),s=String(i.shorthand),a=String(i.capture),o=String(i.wrap),l=r+":"+e+"="+n+s+a+o;if(Wt.cache.hasOwnProperty(l))return Wt.cache[l].result;let c=Math.min(r,e),f=Math.max(r,e);if(Math.abs(c-f)===1){let v=r+"|"+e;return i.capture?`(${v})`:i.wrap===!1?v:`(?:${v})`}let d=gm(r)||gm(e),p={min:r,max:e,a:c,b:f},h=[],b=[];if(d&&(p.isPadded=d,p.maxLen=String(p.max).length),c<0){let v=f<0?Math.abs(f):1;b=cm(v,Math.abs(c),p,i),c=p.a=0}return f>=0&&(h=cm(c,f,p,i)),p.negatives=b,p.positives=h,p.result=K_(b,h,i),i.capture===!0?p.result=`(${p.result})`:i.wrap!==!1&&h.length+b.length>1&&(p.result=`(?:${p.result})`),Wt.cache[l]=p,p.result};function K_(r,e,t){let i=ul(r,e,"-",!1,t)||[],n=ul(e,r,"",!1,t)||[],s=ul(r,e,"-?",!0,t)||[];return i.concat(s).concat(n).join("|")}function X_(r,e){let t=1,i=1,n=dm(r,t),s=new Set([e]);for(;r<=n&&n<=e;)s.add(n),t+=1,n=dm(r,t);for(n=hm(e+1,i)-1;r1&&o.count.pop(),o.count.push(f.count[0]),o.string=o.pattern+mm(o.count),a=c+1;continue}t.isPadded&&(d=rE(c,t,i)),f.string=d+f.pattern+mm(f.count),s.push(f),a=c+1,o=f}return s}function ul(r,e,t,i,n){let s=[];for(let a of r){let{string:o}=a;!i&&!pm(e,"string",o)&&s.push(t+o),i&&pm(e,"string",o)&&s.push(t+o)}return s}function J_(r,e){let t=[];for(let i=0;ie?1:e>r?-1:0}function pm(r,e,t){return r.some(i=>i[e]===t)}function dm(r,e){return Number(String(r).slice(0,-e)+"9".repeat(e))}function hm(r,e){return r-r%Math.pow(10,e)}function mm(r){let[e=0,t=""]=r;return t||e>1?`{${e+(t?","+t:"")}}`:""}function tE(r,e,t){return`[${r}${e-r==1?"":"-"}${e}]`}function gm(r){return/^-?(0+)\d/.test(r)}function rE(r,e,t){if(!e.isPadded)return r;let i=Math.abs(e.maxLen-String(r).length),n=t.relaxZeros!==!1;switch(i){case 0:return"";case 1:return n?"0?":"0";case 2:return n?"0{0,2}":"00";default:return n?`0{0,${i}}`:`0{${i}}`}}Wt.cache={};Wt.clearCache=()=>Wt.cache={};ym.exports=Wt});var pl=x((t6,Cm)=>{u();"use strict";var iE=(Fn(),Bn),wm=bm(),vm=r=>r!==null&&typeof r=="object"&&!Array.isArray(r),nE=r=>e=>r===!0?Number(e):String(e),fl=r=>typeof r=="number"||typeof r=="string"&&r!=="",Ii=r=>Number.isInteger(+r),cl=r=>{let e=`${r}`,t=-1;if(e[0]==="-"&&(e=e.slice(1)),e==="0")return!1;for(;e[++t]==="0";);return t>0},sE=(r,e,t)=>typeof r=="string"||typeof e=="string"?!0:t.stringify===!0,aE=(r,e,t)=>{if(e>0){let i=r[0]==="-"?"-":"";i&&(r=r.slice(1)),r=i+r.padStart(i?e-1:e,"0")}return t===!1?String(r):r},ms=(r,e)=>{let t=r[0]==="-"?"-":"";for(t&&(r=r.slice(1),e--);r.length{r.negatives.sort((o,l)=>ol?1:0),r.positives.sort((o,l)=>ol?1:0);let i=e.capture?"":"?:",n="",s="",a;return r.positives.length&&(n=r.positives.map(o=>ms(String(o),t)).join("|")),r.negatives.length&&(s=`-(${i}${r.negatives.map(o=>ms(String(o),t)).join("|")})`),n&&s?a=`${n}|${s}`:a=n||s,e.wrap?`(${i}${a})`:a},xm=(r,e,t,i)=>{if(t)return wm(r,e,{wrap:!1,...i});let n=String.fromCharCode(r);if(r===e)return n;let s=String.fromCharCode(e);return`[${n}-${s}]`},km=(r,e,t)=>{if(Array.isArray(r)){let i=t.wrap===!0,n=t.capture?"":"?:";return i?`(${n}${r.join("|")})`:r.join("|")}return wm(r,e,t)},Sm=(...r)=>new RangeError("Invalid range arguments: "+iE.inspect(...r)),Am=(r,e,t)=>{if(t.strictRanges===!0)throw Sm([r,e]);return[]},lE=(r,e)=>{if(e.strictRanges===!0)throw new TypeError(`Expected step "${r}" to be a number`);return[]},uE=(r,e,t=1,i={})=>{let n=Number(r),s=Number(e);if(!Number.isInteger(n)||!Number.isInteger(s)){if(i.strictRanges===!0)throw Sm([r,e]);return[]}n===0&&(n=0),s===0&&(s=0);let a=n>s,o=String(r),l=String(e),c=String(t);t=Math.max(Math.abs(t),1);let f=cl(o)||cl(l)||cl(c),d=f?Math.max(o.length,l.length,c.length):0,p=f===!1&&sE(r,e,i)===!1,h=i.transform||nE(p);if(i.toRegex&&t===1)return xm(ms(r,d),ms(e,d),!0,i);let b={negatives:[],positives:[]},v=k=>b[k<0?"negatives":"positives"].push(Math.abs(k)),y=[],w=0;for(;a?n>=s:n<=s;)i.toRegex===!0&&t>1?v(n):y.push(aE(h(n,w),d,p)),n=a?n-t:n+t,w++;return i.toRegex===!0?t>1?oE(b,i,d):km(y,null,{wrap:!1,...i}):y},fE=(r,e,t=1,i={})=>{if(!Ii(r)&&r.length>1||!Ii(e)&&e.length>1)return Am(r,e,i);let n=i.transform||(p=>String.fromCharCode(p)),s=`${r}`.charCodeAt(0),a=`${e}`.charCodeAt(0),o=s>a,l=Math.min(s,a),c=Math.max(s,a);if(i.toRegex&&t===1)return xm(l,c,!1,i);let f=[],d=0;for(;o?s>=a:s<=a;)f.push(n(s,d)),s=o?s-t:s+t,d++;return i.toRegex===!0?km(f,null,{wrap:!1,options:i}):f},gs=(r,e,t,i={})=>{if(e==null&&fl(r))return[r];if(!fl(r)||!fl(e))return Am(r,e,i);if(typeof t=="function")return gs(r,e,1,{transform:t});if(vm(t))return gs(r,e,0,t);let n={...i};return n.capture===!0&&(n.wrap=!0),t=t||n.step||1,Ii(t)?Ii(r)&&Ii(e)?uE(r,e,t,n):fE(r,e,Math.max(Math.abs(t),1),n):t!=null&&!vm(t)?lE(t,n):gs(r,e,1,t)};Cm.exports=gs});var Om=x((r6,Em)=>{u();"use strict";var cE=pl(),_m=ds(),pE=(r,e={})=>{let t=(i,n={})=>{let s=_m.isInvalidBrace(n),a=i.invalid===!0&&e.escapeInvalid===!0,o=s===!0||a===!0,l=e.escapeInvalid===!0?"\\":"",c="";if(i.isOpen===!0)return l+i.value;if(i.isClose===!0)return console.log("node.isClose",l,i.value),l+i.value;if(i.type==="open")return o?l+i.value:"(";if(i.type==="close")return o?l+i.value:")";if(i.type==="comma")return i.prev.type==="comma"?"":o?i.value:"|";if(i.value)return i.value;if(i.nodes&&i.ranges>0){let f=_m.reduce(i.nodes),d=cE(...f,{...e,wrap:!1,toRegex:!0,strictZeros:!0});if(d.length!==0)return f.length>1&&d.length>1?`(${d})`:d}if(i.nodes)for(let f of i.nodes)c+=t(f,i);return c};return t(r)};Em.exports=pE});var Pm=x((i6,Rm)=>{u();"use strict";var dE=pl(),Tm=hs(),mr=ds(),Gt=(r="",e="",t=!1)=>{let i=[];if(r=[].concat(r),e=[].concat(e),!e.length)return r;if(!r.length)return t?mr.flatten(e).map(n=>`{${n}}`):e;for(let n of r)if(Array.isArray(n))for(let s of n)i.push(Gt(s,e,t));else for(let s of e)t===!0&&typeof s=="string"&&(s=`{${s}}`),i.push(Array.isArray(s)?Gt(n,s,t):n+s);return mr.flatten(i)},hE=(r,e={})=>{let t=e.rangeLimit===void 0?1e3:e.rangeLimit,i=(n,s={})=>{n.queue=[];let a=s,o=s.queue;for(;a.type!=="brace"&&a.type!=="root"&&a.parent;)a=a.parent,o=a.queue;if(n.invalid||n.dollar){o.push(Gt(o.pop(),Tm(n,e)));return}if(n.type==="brace"&&n.invalid!==!0&&n.nodes.length===2){o.push(Gt(o.pop(),["{}"]));return}if(n.nodes&&n.ranges>0){let d=mr.reduce(n.nodes);if(mr.exceedsLimit(...d,e.step,t))throw new RangeError("expanded array length exceeds range limit. Use options.rangeLimit to increase or disable the limit.");let p=dE(...d,e);p.length===0&&(p=Tm(n,e)),o.push(Gt(o.pop(),p)),n.nodes=[];return}let l=mr.encloseBrace(n),c=n.queue,f=n;for(;f.type!=="brace"&&f.type!=="root"&&f.parent;)f=f.parent,c=f.queue;for(let d=0;d{u();"use strict";Im.exports={MAX_LENGTH:1e4,CHAR_0:"0",CHAR_9:"9",CHAR_UPPERCASE_A:"A",CHAR_LOWERCASE_A:"a",CHAR_UPPERCASE_Z:"Z",CHAR_LOWERCASE_Z:"z",CHAR_LEFT_PARENTHESES:"(",CHAR_RIGHT_PARENTHESES:")",CHAR_ASTERISK:"*",CHAR_AMPERSAND:"&",CHAR_AT:"@",CHAR_BACKSLASH:"\\",CHAR_BACKTICK:"`",CHAR_CARRIAGE_RETURN:"\r",CHAR_CIRCUMFLEX_ACCENT:"^",CHAR_COLON:":",CHAR_COMMA:",",CHAR_DOLLAR:"$",CHAR_DOT:".",CHAR_DOUBLE_QUOTE:'"',CHAR_EQUAL:"=",CHAR_EXCLAMATION_MARK:"!",CHAR_FORM_FEED:"\f",CHAR_FORWARD_SLASH:"/",CHAR_HASH:"#",CHAR_HYPHEN_MINUS:"-",CHAR_LEFT_ANGLE_BRACKET:"<",CHAR_LEFT_CURLY_BRACE:"{",CHAR_LEFT_SQUARE_BRACKET:"[",CHAR_LINE_FEED:` +`,CHAR_NO_BREAK_SPACE:"\xA0",CHAR_PERCENT:"%",CHAR_PLUS:"+",CHAR_QUESTION_MARK:"?",CHAR_RIGHT_ANGLE_BRACKET:">",CHAR_RIGHT_CURLY_BRACE:"}",CHAR_RIGHT_SQUARE_BRACKET:"]",CHAR_SEMICOLON:";",CHAR_SINGLE_QUOTE:"'",CHAR_SPACE:" ",CHAR_TAB:" ",CHAR_UNDERSCORE:"_",CHAR_VERTICAL_LINE:"|",CHAR_ZERO_WIDTH_NOBREAK_SPACE:"\uFEFF"}});var Nm=x((s6,Mm)=>{u();"use strict";var mE=hs(),{MAX_LENGTH:qm,CHAR_BACKSLASH:dl,CHAR_BACKTICK:gE,CHAR_COMMA:yE,CHAR_DOT:bE,CHAR_LEFT_PARENTHESES:wE,CHAR_RIGHT_PARENTHESES:vE,CHAR_LEFT_CURLY_BRACE:xE,CHAR_RIGHT_CURLY_BRACE:kE,CHAR_LEFT_SQUARE_BRACKET:$m,CHAR_RIGHT_SQUARE_BRACKET:Lm,CHAR_DOUBLE_QUOTE:SE,CHAR_SINGLE_QUOTE:AE,CHAR_NO_BREAK_SPACE:CE,CHAR_ZERO_WIDTH_NOBREAK_SPACE:_E}=Dm(),EE=(r,e={})=>{if(typeof r!="string")throw new TypeError("Expected a string");let t=e||{},i=typeof t.maxLength=="number"?Math.min(qm,t.maxLength):qm;if(r.length>i)throw new SyntaxError(`Input length (${r.length}), exceeds max characters (${i})`);let n={type:"root",input:r,nodes:[]},s=[n],a=n,o=n,l=0,c=r.length,f=0,d=0,p,h=()=>r[f++],b=v=>{if(v.type==="text"&&o.type==="dot"&&(o.type="text"),o&&o.type==="text"&&v.type==="text"){o.value+=v.value;return}return a.nodes.push(v),v.parent=a,v.prev=o,o=v,v};for(b({type:"bos"});f0){if(a.ranges>0){a.ranges=0;let v=a.nodes.shift();a.nodes=[v,{type:"text",value:mE(a)}]}b({type:"comma",value:p}),a.commas++;continue}if(p===bE&&d>0&&a.commas===0){let v=a.nodes;if(d===0||v.length===0){b({type:"text",value:p});continue}if(o.type==="dot"){if(a.range=[],o.value+=p,o.type="range",a.nodes.length!==3&&a.nodes.length!==5){a.invalid=!0,a.ranges=0,o.type="text";continue}a.ranges++,a.args=[];continue}if(o.type==="range"){v.pop();let y=v[v.length-1];y.value+=o.value+p,o=y,a.ranges--;continue}b({type:"dot",value:p});continue}b({type:"text",value:p})}do if(a=s.pop(),a.type!=="root"){a.nodes.forEach(w=>{w.nodes||(w.type==="open"&&(w.isOpen=!0),w.type==="close"&&(w.isClose=!0),w.nodes||(w.type="text"),w.invalid=!0)});let v=s[s.length-1],y=v.nodes.indexOf(a);v.nodes.splice(y,1,...a.nodes)}while(s.length>0);return b({type:"eos"}),n};Mm.exports=EE});var jm=x((a6,Fm)=>{u();"use strict";var Bm=hs(),OE=Om(),TE=Pm(),RE=Nm(),Le=(r,e={})=>{let t=[];if(Array.isArray(r))for(let i of r){let n=Le.create(i,e);Array.isArray(n)?t.push(...n):t.push(n)}else t=[].concat(Le.create(r,e));return e&&e.expand===!0&&e.nodupes===!0&&(t=[...new Set(t)]),t};Le.parse=(r,e={})=>RE(r,e);Le.stringify=(r,e={})=>typeof r=="string"?Bm(Le.parse(r,e),e):Bm(r,e);Le.compile=(r,e={})=>(typeof r=="string"&&(r=Le.parse(r,e)),OE(r,e));Le.expand=(r,e={})=>{typeof r=="string"&&(r=Le.parse(r,e));let t=TE(r,e);return e.noempty===!0&&(t=t.filter(Boolean)),e.nodupes===!0&&(t=[...new Set(t)]),t};Le.create=(r,e={})=>r===""||r.length<3?[r]:e.expand!==!0?Le.compile(r,e):Le.expand(r,e);Fm.exports=Le});var Di=x((o6,Wm)=>{u();"use strict";var PE=(et(),Ur),at="\\\\/",zm=`[^${at}]`,yt="\\.",IE="\\+",DE="\\?",ys="\\/",qE="(?=.)",Um="[^/]",hl=`(?:${ys}|$)`,Vm=`(?:^|${ys})`,ml=`${yt}{1,2}${hl}`,$E=`(?!${yt})`,LE=`(?!${Vm}${ml})`,ME=`(?!${yt}{0,1}${hl})`,NE=`(?!${ml})`,BE=`[^.${ys}]`,FE=`${Um}*?`,Hm={DOT_LITERAL:yt,PLUS_LITERAL:IE,QMARK_LITERAL:DE,SLASH_LITERAL:ys,ONE_CHAR:qE,QMARK:Um,END_ANCHOR:hl,DOTS_SLASH:ml,NO_DOT:$E,NO_DOTS:LE,NO_DOT_SLASH:ME,NO_DOTS_SLASH:NE,QMARK_NO_DOT:BE,STAR:FE,START_ANCHOR:Vm},jE={...Hm,SLASH_LITERAL:`[${at}]`,QMARK:zm,STAR:`${zm}*?`,DOTS_SLASH:`${yt}{1,2}(?:[${at}]|$)`,NO_DOT:`(?!${yt})`,NO_DOTS:`(?!(?:^|[${at}])${yt}{1,2}(?:[${at}]|$))`,NO_DOT_SLASH:`(?!${yt}{0,1}(?:[${at}]|$))`,NO_DOTS_SLASH:`(?!${yt}{1,2}(?:[${at}]|$))`,QMARK_NO_DOT:`[^.${at}]`,START_ANCHOR:`(?:^|[${at}])`,END_ANCHOR:`(?:[${at}]|$)`},zE={alnum:"a-zA-Z0-9",alpha:"a-zA-Z",ascii:"\\x00-\\x7F",blank:" \\t",cntrl:"\\x00-\\x1F\\x7F",digit:"0-9",graph:"\\x21-\\x7E",lower:"a-z",print:"\\x20-\\x7E ",punct:"\\-!\"#$%&'()\\*+,./:;<=>?@[\\]^_`{|}~",space:" \\t\\r\\n\\v\\f",upper:"A-Z",word:"A-Za-z0-9_",xdigit:"A-Fa-f0-9"};Wm.exports={MAX_LENGTH:1024*64,POSIX_REGEX_SOURCE:zE,REGEX_BACKSLASH:/\\(?![*+?^${}(|)[\]])/g,REGEX_NON_SPECIAL_CHARS:/^[^@![\].,$*+?^{}()|\\/]+/,REGEX_SPECIAL_CHARS:/[-*+?.^${}(|)[\]]/,REGEX_SPECIAL_CHARS_BACKREF:/(\\?)((\W)(\3*))/g,REGEX_SPECIAL_CHARS_GLOBAL:/([-*+?.^${}(|)[\]])/g,REGEX_REMOVE_BACKSLASH:/(?:\[.*?[^\\]\]|\\(?=.))/g,REPLACEMENTS:{"***":"*","**/**":"**","**/**/**":"**"},CHAR_0:48,CHAR_9:57,CHAR_UPPERCASE_A:65,CHAR_LOWERCASE_A:97,CHAR_UPPERCASE_Z:90,CHAR_LOWERCASE_Z:122,CHAR_LEFT_PARENTHESES:40,CHAR_RIGHT_PARENTHESES:41,CHAR_ASTERISK:42,CHAR_AMPERSAND:38,CHAR_AT:64,CHAR_BACKWARD_SLASH:92,CHAR_CARRIAGE_RETURN:13,CHAR_CIRCUMFLEX_ACCENT:94,CHAR_COLON:58,CHAR_COMMA:44,CHAR_DOT:46,CHAR_DOUBLE_QUOTE:34,CHAR_EQUAL:61,CHAR_EXCLAMATION_MARK:33,CHAR_FORM_FEED:12,CHAR_FORWARD_SLASH:47,CHAR_GRAVE_ACCENT:96,CHAR_HASH:35,CHAR_HYPHEN_MINUS:45,CHAR_LEFT_ANGLE_BRACKET:60,CHAR_LEFT_CURLY_BRACE:123,CHAR_LEFT_SQUARE_BRACKET:91,CHAR_LINE_FEED:10,CHAR_NO_BREAK_SPACE:160,CHAR_PERCENT:37,CHAR_PLUS:43,CHAR_QUESTION_MARK:63,CHAR_RIGHT_ANGLE_BRACKET:62,CHAR_RIGHT_CURLY_BRACE:125,CHAR_RIGHT_SQUARE_BRACKET:93,CHAR_SEMICOLON:59,CHAR_SINGLE_QUOTE:39,CHAR_SPACE:32,CHAR_TAB:9,CHAR_UNDERSCORE:95,CHAR_VERTICAL_LINE:124,CHAR_ZERO_WIDTH_NOBREAK_SPACE:65279,SEP:PE.sep,extglobChars(r){return{"!":{type:"negate",open:"(?:(?!(?:",close:`))${r.STAR})`},"?":{type:"qmark",open:"(?:",close:")?"},"+":{type:"plus",open:"(?:",close:")+"},"*":{type:"star",open:"(?:",close:")*"},"@":{type:"at",open:"(?:",close:")"}}},globChars(r){return r===!0?jE:Hm}}});var qi=x(Re=>{u();"use strict";var UE=(et(),Ur),VE=m.platform==="win32",{REGEX_BACKSLASH:HE,REGEX_REMOVE_BACKSLASH:WE,REGEX_SPECIAL_CHARS:GE,REGEX_SPECIAL_CHARS_GLOBAL:QE}=Di();Re.isObject=r=>r!==null&&typeof r=="object"&&!Array.isArray(r);Re.hasRegexChars=r=>GE.test(r);Re.isRegexChar=r=>r.length===1&&Re.hasRegexChars(r);Re.escapeRegex=r=>r.replace(QE,"\\$1");Re.toPosixSlashes=r=>r.replace(HE,"/");Re.removeBackslashes=r=>r.replace(WE,e=>e==="\\"?"":e);Re.supportsLookbehinds=()=>{let r=m.version.slice(1).split(".").map(Number);return r.length===3&&r[0]>=9||r[0]===8&&r[1]>=10};Re.isWindows=r=>r&&typeof r.windows=="boolean"?r.windows:VE===!0||UE.sep==="\\";Re.escapeLast=(r,e,t)=>{let i=r.lastIndexOf(e,t);return i===-1?r:r[i-1]==="\\"?Re.escapeLast(r,e,i-1):`${r.slice(0,i)}\\${r.slice(i)}`};Re.removePrefix=(r,e={})=>{let t=r;return t.startsWith("./")&&(t=t.slice(2),e.prefix="./"),t};Re.wrapOutput=(r,e={},t={})=>{let i=t.contains?"":"^",n=t.contains?"":"$",s=`${i}(?:${r})${n}`;return e.negated===!0&&(s=`(?:^(?!${s}).*$)`),s}});var eg=x((u6,Jm)=>{u();"use strict";var Gm=qi(),{CHAR_ASTERISK:gl,CHAR_AT:YE,CHAR_BACKWARD_SLASH:$i,CHAR_COMMA:KE,CHAR_DOT:yl,CHAR_EXCLAMATION_MARK:bl,CHAR_FORWARD_SLASH:Qm,CHAR_LEFT_CURLY_BRACE:wl,CHAR_LEFT_PARENTHESES:vl,CHAR_LEFT_SQUARE_BRACKET:XE,CHAR_PLUS:ZE,CHAR_QUESTION_MARK:Ym,CHAR_RIGHT_CURLY_BRACE:JE,CHAR_RIGHT_PARENTHESES:Km,CHAR_RIGHT_SQUARE_BRACKET:e2}=Di(),Xm=r=>r===Qm||r===$i,Zm=r=>{r.isPrefix!==!0&&(r.depth=r.isGlobstar?1/0:1)},t2=(r,e)=>{let t=e||{},i=r.length-1,n=t.parts===!0||t.scanToEnd===!0,s=[],a=[],o=[],l=r,c=-1,f=0,d=0,p=!1,h=!1,b=!1,v=!1,y=!1,w=!1,k=!1,S=!1,E=!1,T=!1,B=0,N,R,F={value:"",depth:0,isGlob:!1},Y=()=>c>=i,_=()=>l.charCodeAt(c+1),Q=()=>(N=R,l.charCodeAt(++c));for(;c0&&(le=l.slice(0,f),l=l.slice(f),d-=f),U&&b===!0&&d>0?(U=l.slice(0,d),A=l.slice(d)):b===!0?(U="",A=l):U=l,U&&U!==""&&U!=="/"&&U!==l&&Xm(U.charCodeAt(U.length-1))&&(U=U.slice(0,-1)),t.unescape===!0&&(A&&(A=Gm.removeBackslashes(A)),U&&k===!0&&(U=Gm.removeBackslashes(U)));let C={prefix:le,input:r,start:f,base:U,glob:A,isBrace:p,isBracket:h,isGlob:b,isExtglob:v,isGlobstar:y,negated:S,negatedExtglob:E};if(t.tokens===!0&&(C.maxDepth=0,Xm(R)||a.push(F),C.tokens=a),t.parts===!0||t.tokens===!0){let he;for(let V=0;V{u();"use strict";var bs=Di(),Me=qi(),{MAX_LENGTH:ws,POSIX_REGEX_SOURCE:r2,REGEX_NON_SPECIAL_CHARS:i2,REGEX_SPECIAL_CHARS_BACKREF:n2,REPLACEMENTS:tg}=bs,s2=(r,e)=>{if(typeof e.expandRange=="function")return e.expandRange(...r,e);r.sort();let t=`[${r.join("-")}]`;try{new RegExp(t)}catch(i){return r.map(n=>Me.escapeRegex(n)).join("..")}return t},gr=(r,e)=>`Missing ${r}: "${e}" - use "\\\\${e}" to match literal characters`,xl=(r,e)=>{if(typeof r!="string")throw new TypeError("Expected a string");r=tg[r]||r;let t={...e},i=typeof t.maxLength=="number"?Math.min(ws,t.maxLength):ws,n=r.length;if(n>i)throw new SyntaxError(`Input length: ${n}, exceeds maximum allowed length: ${i}`);let s={type:"bos",value:"",output:t.prepend||""},a=[s],o=t.capture?"":"?:",l=Me.isWindows(e),c=bs.globChars(l),f=bs.extglobChars(c),{DOT_LITERAL:d,PLUS_LITERAL:p,SLASH_LITERAL:h,ONE_CHAR:b,DOTS_SLASH:v,NO_DOT:y,NO_DOT_SLASH:w,NO_DOTS_SLASH:k,QMARK:S,QMARK_NO_DOT:E,STAR:T,START_ANCHOR:B}=c,N=$=>`(${o}(?:(?!${B}${$.dot?v:d}).)*?)`,R=t.dot?"":y,F=t.dot?S:E,Y=t.bash===!0?N(t):T;t.capture&&(Y=`(${Y})`),typeof t.noext=="boolean"&&(t.noextglob=t.noext);let _={input:r,index:-1,start:0,dot:t.dot===!0,consumed:"",output:"",prefix:"",backtrack:!1,negated:!1,brackets:0,braces:0,parens:0,quotes:0,globstar:!1,tokens:a};r=Me.removePrefix(r,_),n=r.length;let Q=[],U=[],le=[],A=s,C,he=()=>_.index===n-1,V=_.peek=($=1)=>r[_.index+$],Ee=_.advance=()=>r[++_.index]||"",Ie=()=>r.slice(_.index+1),De=($="",ae=0)=>{_.consumed+=$,_.index+=ae},ji=$=>{_.output+=$.output!=null?$.output:$.value,De($.value)},Iv=()=>{let $=1;for(;V()==="!"&&(V(2)!=="("||V(3)==="?");)Ee(),_.start++,$++;return $%2==0?!1:(_.negated=!0,_.start++,!0)},zi=$=>{_[$]++,le.push($)},Ft=$=>{_[$]--,le.pop()},W=$=>{if(A.type==="globstar"){let ae=_.braces>0&&($.type==="comma"||$.type==="brace"),I=$.extglob===!0||Q.length&&($.type==="pipe"||$.type==="paren");$.type!=="slash"&&$.type!=="paren"&&!ae&&!I&&(_.output=_.output.slice(0,-A.output.length),A.type="star",A.value="*",A.output=Y,_.output+=A.output)}if(Q.length&&$.type!=="paren"&&(Q[Q.length-1].inner+=$.value),($.value||$.output)&&ji($),A&&A.type==="text"&&$.type==="text"){A.value+=$.value,A.output=(A.output||"")+$.value;return}$.prev=A,a.push($),A=$},Ui=($,ae)=>{let I={...f[ae],conditions:1,inner:""};I.prev=A,I.parens=_.parens,I.output=_.output;let H=(t.capture?"(":"")+I.open;zi("parens"),W({type:$,value:ae,output:_.output?"":b}),W({type:"paren",extglob:!0,value:Ee(),output:H}),Q.push(I)},Dv=$=>{let ae=$.close+(t.capture?")":""),I;if($.type==="negate"){let H=Y;if($.inner&&$.inner.length>1&&$.inner.includes("/")&&(H=N(t)),(H!==Y||he()||/^\)+$/.test(Ie()))&&(ae=$.close=`)$))${H}`),$.inner.includes("*")&&(I=Ie())&&/^\.[^\\/.]+$/.test(I)){let ce=xl(I,{...e,fastpaths:!1}).output;ae=$.close=`)${ce})${H})`}$.prev.type==="bos"&&(_.negatedExtglob=!0)}W({type:"paren",extglob:!0,value:C,output:ae}),Ft("parens")};if(t.fastpaths!==!1&&!/(^[*!]|[/()[\]{}"])/.test(r)){let $=!1,ae=r.replace(n2,(I,H,ce,Ce,ye,Bs)=>Ce==="\\"?($=!0,I):Ce==="?"?H?H+Ce+(ye?S.repeat(ye.length):""):Bs===0?F+(ye?S.repeat(ye.length):""):S.repeat(ce.length):Ce==="."?d.repeat(ce.length):Ce==="*"?H?H+Ce+(ye?Y:""):Y:H?I:`\\${I}`);return $===!0&&(t.unescape===!0?ae=ae.replace(/\\/g,""):ae=ae.replace(/\\+/g,I=>I.length%2==0?"\\\\":I?"\\":"")),ae===r&&t.contains===!0?(_.output=r,_):(_.output=Me.wrapOutput(ae,_,e),_)}for(;!he();){if(C=Ee(),C==="\0")continue;if(C==="\\"){let I=V();if(I==="/"&&t.bash!==!0||I==="."||I===";")continue;if(!I){C+="\\",W({type:"text",value:C});continue}let H=/^\\+/.exec(Ie()),ce=0;if(H&&H[0].length>2&&(ce=H[0].length,_.index+=ce,ce%2!=0&&(C+="\\")),t.unescape===!0?C=Ee():C+=Ee(),_.brackets===0){W({type:"text",value:C});continue}}if(_.brackets>0&&(C!=="]"||A.value==="["||A.value==="[^")){if(t.posix!==!1&&C===":"){let I=A.value.slice(1);if(I.includes("[")&&(A.posix=!0,I.includes(":"))){let H=A.value.lastIndexOf("["),ce=A.value.slice(0,H),Ce=A.value.slice(H+2),ye=r2[Ce];if(ye){A.value=ce+ye,_.backtrack=!0,Ee(),!s.output&&a.indexOf(A)===1&&(s.output=b);continue}}}(C==="["&&V()!==":"||C==="-"&&V()==="]")&&(C=`\\${C}`),C==="]"&&(A.value==="["||A.value==="[^")&&(C=`\\${C}`),t.posix===!0&&C==="!"&&A.value==="["&&(C="^"),A.value+=C,ji({value:C});continue}if(_.quotes===1&&C!=='"'){C=Me.escapeRegex(C),A.value+=C,ji({value:C});continue}if(C==='"'){_.quotes=_.quotes===1?0:1,t.keepQuotes===!0&&W({type:"text",value:C});continue}if(C==="("){zi("parens"),W({type:"paren",value:C});continue}if(C===")"){if(_.parens===0&&t.strictBrackets===!0)throw new SyntaxError(gr("opening","("));let I=Q[Q.length-1];if(I&&_.parens===I.parens+1){Dv(Q.pop());continue}W({type:"paren",value:C,output:_.parens?")":"\\)"}),Ft("parens");continue}if(C==="["){if(t.nobracket===!0||!Ie().includes("]")){if(t.nobracket!==!0&&t.strictBrackets===!0)throw new SyntaxError(gr("closing","]"));C=`\\${C}`}else zi("brackets");W({type:"bracket",value:C});continue}if(C==="]"){if(t.nobracket===!0||A&&A.type==="bracket"&&A.value.length===1){W({type:"text",value:C,output:`\\${C}`});continue}if(_.brackets===0){if(t.strictBrackets===!0)throw new SyntaxError(gr("opening","["));W({type:"text",value:C,output:`\\${C}`});continue}Ft("brackets");let I=A.value.slice(1);if(A.posix!==!0&&I[0]==="^"&&!I.includes("/")&&(C=`/${C}`),A.value+=C,ji({value:C}),t.literalBrackets===!1||Me.hasRegexChars(I))continue;let H=Me.escapeRegex(A.value);if(_.output=_.output.slice(0,-A.value.length),t.literalBrackets===!0){_.output+=H,A.value=H;continue}A.value=`(${o}${H}|${A.value})`,_.output+=A.value;continue}if(C==="{"&&t.nobrace!==!0){zi("braces");let I={type:"brace",value:C,output:"(",outputIndex:_.output.length,tokensIndex:_.tokens.length};U.push(I),W(I);continue}if(C==="}"){let I=U[U.length-1];if(t.nobrace===!0||!I){W({type:"text",value:C,output:C});continue}let H=")";if(I.dots===!0){let ce=a.slice(),Ce=[];for(let ye=ce.length-1;ye>=0&&(a.pop(),ce[ye].type!=="brace");ye--)ce[ye].type!=="dots"&&Ce.unshift(ce[ye].value);H=s2(Ce,t),_.backtrack=!0}if(I.comma!==!0&&I.dots!==!0){let ce=_.output.slice(0,I.outputIndex),Ce=_.tokens.slice(I.tokensIndex);I.value=I.output="\\{",C=H="\\}",_.output=ce;for(let ye of Ce)_.output+=ye.output||ye.value}W({type:"brace",value:C,output:H}),Ft("braces"),U.pop();continue}if(C==="|"){Q.length>0&&Q[Q.length-1].conditions++,W({type:"text",value:C});continue}if(C===","){let I=C,H=U[U.length-1];H&&le[le.length-1]==="braces"&&(H.comma=!0,I="|"),W({type:"comma",value:C,output:I});continue}if(C==="/"){if(A.type==="dot"&&_.index===_.start+1){_.start=_.index+1,_.consumed="",_.output="",a.pop(),A=s;continue}W({type:"slash",value:C,output:h});continue}if(C==="."){if(_.braces>0&&A.type==="dot"){A.value==="."&&(A.output=d);let I=U[U.length-1];A.type="dots",A.output+=C,A.value+=C,I.dots=!0;continue}if(_.braces+_.parens===0&&A.type!=="bos"&&A.type!=="slash"){W({type:"text",value:C,output:d});continue}W({type:"dot",value:C,output:d});continue}if(C==="?"){if(!(A&&A.value==="(")&&t.noextglob!==!0&&V()==="("&&V(2)!=="?"){Ui("qmark",C);continue}if(A&&A.type==="paren"){let H=V(),ce=C;if(H==="<"&&!Me.supportsLookbehinds())throw new Error("Node.js v10 or higher is required for regex lookbehinds");(A.value==="("&&!/[!=<:]/.test(H)||H==="<"&&!/<([!=]|\w+>)/.test(Ie()))&&(ce=`\\${C}`),W({type:"text",value:C,output:ce});continue}if(t.dot!==!0&&(A.type==="slash"||A.type==="bos")){W({type:"qmark",value:C,output:E});continue}W({type:"qmark",value:C,output:S});continue}if(C==="!"){if(t.noextglob!==!0&&V()==="("&&(V(2)!=="?"||!/[!=<:]/.test(V(3)))){Ui("negate",C);continue}if(t.nonegate!==!0&&_.index===0){Iv();continue}}if(C==="+"){if(t.noextglob!==!0&&V()==="("&&V(2)!=="?"){Ui("plus",C);continue}if(A&&A.value==="("||t.regex===!1){W({type:"plus",value:C,output:p});continue}if(A&&(A.type==="bracket"||A.type==="paren"||A.type==="brace")||_.parens>0){W({type:"plus",value:C});continue}W({type:"plus",value:p});continue}if(C==="@"){if(t.noextglob!==!0&&V()==="("&&V(2)!=="?"){W({type:"at",extglob:!0,value:C,output:""});continue}W({type:"text",value:C});continue}if(C!=="*"){(C==="$"||C==="^")&&(C=`\\${C}`);let I=i2.exec(Ie());I&&(C+=I[0],_.index+=I[0].length),W({type:"text",value:C});continue}if(A&&(A.type==="globstar"||A.star===!0)){A.type="star",A.star=!0,A.value+=C,A.output=Y,_.backtrack=!0,_.globstar=!0,De(C);continue}let $=Ie();if(t.noextglob!==!0&&/^\([^?]/.test($)){Ui("star",C);continue}if(A.type==="star"){if(t.noglobstar===!0){De(C);continue}let I=A.prev,H=I.prev,ce=I.type==="slash"||I.type==="bos",Ce=H&&(H.type==="star"||H.type==="globstar");if(t.bash===!0&&(!ce||$[0]&&$[0]!=="/")){W({type:"star",value:C,output:""});continue}let ye=_.braces>0&&(I.type==="comma"||I.type==="brace"),Bs=Q.length&&(I.type==="pipe"||I.type==="paren");if(!ce&&I.type!=="paren"&&!ye&&!Bs){W({type:"star",value:C,output:""});continue}for(;$.slice(0,3)==="/**";){let Vi=r[_.index+4];if(Vi&&Vi!=="/")break;$=$.slice(3),De("/**",3)}if(I.type==="bos"&&he()){A.type="globstar",A.value+=C,A.output=N(t),_.output=A.output,_.globstar=!0,De(C);continue}if(I.type==="slash"&&I.prev.type!=="bos"&&!Ce&&he()){_.output=_.output.slice(0,-(I.output+A.output).length),I.output=`(?:${I.output}`,A.type="globstar",A.output=N(t)+(t.strictSlashes?")":"|$)"),A.value+=C,_.globstar=!0,_.output+=I.output+A.output,De(C);continue}if(I.type==="slash"&&I.prev.type!=="bos"&&$[0]==="/"){let Vi=$[1]!==void 0?"|$":"";_.output=_.output.slice(0,-(I.output+A.output).length),I.output=`(?:${I.output}`,A.type="globstar",A.output=`${N(t)}${h}|${h}${Vi})`,A.value+=C,_.output+=I.output+A.output,_.globstar=!0,De(C+Ee()),W({type:"slash",value:"/",output:""});continue}if(I.type==="bos"&&$[0]==="/"){A.type="globstar",A.value+=C,A.output=`(?:^|${h}|${N(t)}${h})`,_.output=A.output,_.globstar=!0,De(C+Ee()),W({type:"slash",value:"/",output:""});continue}_.output=_.output.slice(0,-A.output.length),A.type="globstar",A.output=N(t),A.value+=C,_.output+=A.output,_.globstar=!0,De(C);continue}let ae={type:"star",value:C,output:Y};if(t.bash===!0){ae.output=".*?",(A.type==="bos"||A.type==="slash")&&(ae.output=R+ae.output),W(ae);continue}if(A&&(A.type==="bracket"||A.type==="paren")&&t.regex===!0){ae.output=C,W(ae);continue}(_.index===_.start||A.type==="slash"||A.type==="dot")&&(A.type==="dot"?(_.output+=w,A.output+=w):t.dot===!0?(_.output+=k,A.output+=k):(_.output+=R,A.output+=R),V()!=="*"&&(_.output+=b,A.output+=b)),W(ae)}for(;_.brackets>0;){if(t.strictBrackets===!0)throw new SyntaxError(gr("closing","]"));_.output=Me.escapeLast(_.output,"["),Ft("brackets")}for(;_.parens>0;){if(t.strictBrackets===!0)throw new SyntaxError(gr("closing",")"));_.output=Me.escapeLast(_.output,"("),Ft("parens")}for(;_.braces>0;){if(t.strictBrackets===!0)throw new SyntaxError(gr("closing","}"));_.output=Me.escapeLast(_.output,"{"),Ft("braces")}if(t.strictSlashes!==!0&&(A.type==="star"||A.type==="bracket")&&W({type:"maybe_slash",value:"",output:`${h}?`}),_.backtrack===!0){_.output="";for(let $ of _.tokens)_.output+=$.output!=null?$.output:$.value,$.suffix&&(_.output+=$.suffix)}return _};xl.fastpaths=(r,e)=>{let t={...e},i=typeof t.maxLength=="number"?Math.min(ws,t.maxLength):ws,n=r.length;if(n>i)throw new SyntaxError(`Input length: ${n}, exceeds maximum allowed length: ${i}`);r=tg[r]||r;let s=Me.isWindows(e),{DOT_LITERAL:a,SLASH_LITERAL:o,ONE_CHAR:l,DOTS_SLASH:c,NO_DOT:f,NO_DOTS:d,NO_DOTS_SLASH:p,STAR:h,START_ANCHOR:b}=bs.globChars(s),v=t.dot?d:f,y=t.dot?p:f,w=t.capture?"":"?:",k={negated:!1,prefix:""},S=t.bash===!0?".*?":h;t.capture&&(S=`(${S})`);let E=R=>R.noglobstar===!0?S:`(${w}(?:(?!${b}${R.dot?c:a}).)*?)`,T=R=>{switch(R){case"*":return`${v}${l}${S}`;case".*":return`${a}${l}${S}`;case"*.*":return`${v}${S}${a}${l}${S}`;case"*/*":return`${v}${S}${o}${l}${y}${S}`;case"**":return v+E(t);case"**/*":return`(?:${v}${E(t)}${o})?${y}${l}${S}`;case"**/*.*":return`(?:${v}${E(t)}${o})?${y}${S}${a}${l}${S}`;case"**/.*":return`(?:${v}${E(t)}${o})?${a}${l}${S}`;default:{let F=/^(.*?)\.(\w+)$/.exec(R);if(!F)return;let Y=T(F[1]);return Y?Y+a+F[2]:void 0}}},B=Me.removePrefix(r,k),N=T(B);return N&&t.strictSlashes!==!0&&(N+=`${o}?`),N};rg.exports=xl});var sg=x((c6,ng)=>{u();"use strict";var a2=(et(),Ur),o2=eg(),kl=ig(),Sl=qi(),l2=Di(),u2=r=>r&&typeof r=="object"&&!Array.isArray(r),de=(r,e,t=!1)=>{if(Array.isArray(r)){let f=r.map(p=>de(p,e,t));return p=>{for(let h of f){let b=h(p);if(b)return b}return!1}}let i=u2(r)&&r.tokens&&r.input;if(r===""||typeof r!="string"&&!i)throw new TypeError("Expected pattern to be a non-empty string");let n=e||{},s=Sl.isWindows(e),a=i?de.compileRe(r,e):de.makeRe(r,e,!1,!0),o=a.state;delete a.state;let l=()=>!1;if(n.ignore){let f={...e,ignore:null,onMatch:null,onResult:null};l=de(n.ignore,f,t)}let c=(f,d=!1)=>{let{isMatch:p,match:h,output:b}=de.test(f,a,e,{glob:r,posix:s}),v={glob:r,state:o,regex:a,posix:s,input:f,output:b,match:h,isMatch:p};return typeof n.onResult=="function"&&n.onResult(v),p===!1?(v.isMatch=!1,d?v:!1):l(f)?(typeof n.onIgnore=="function"&&n.onIgnore(v),v.isMatch=!1,d?v:!1):(typeof n.onMatch=="function"&&n.onMatch(v),d?v:!0)};return t&&(c.state=o),c};de.test=(r,e,t,{glob:i,posix:n}={})=>{if(typeof r!="string")throw new TypeError("Expected input to be a string");if(r==="")return{isMatch:!1,output:""};let s=t||{},a=s.format||(n?Sl.toPosixSlashes:null),o=r===i,l=o&&a?a(r):r;return o===!1&&(l=a?a(r):r,o=l===i),(o===!1||s.capture===!0)&&(s.matchBase===!0||s.basename===!0?o=de.matchBase(r,e,t,n):o=e.exec(l)),{isMatch:Boolean(o),match:o,output:l}};de.matchBase=(r,e,t,i=Sl.isWindows(t))=>(e instanceof RegExp?e:de.makeRe(e,t)).test(a2.basename(r));de.isMatch=(r,e,t)=>de(e,t)(r);de.parse=(r,e)=>Array.isArray(r)?r.map(t=>de.parse(t,e)):kl(r,{...e,fastpaths:!1});de.scan=(r,e)=>o2(r,e);de.compileRe=(r,e,t=!1,i=!1)=>{if(t===!0)return r.output;let n=e||{},s=n.contains?"":"^",a=n.contains?"":"$",o=`${s}(?:${r.output})${a}`;r&&r.negated===!0&&(o=`^(?!${o}).*$`);let l=de.toRegex(o,e);return i===!0&&(l.state=r),l};de.makeRe=(r,e={},t=!1,i=!1)=>{if(!r||typeof r!="string")throw new TypeError("Expected a non-empty string");let n={negated:!1,fastpaths:!0};return e.fastpaths!==!1&&(r[0]==="."||r[0]==="*")&&(n.output=kl.fastpaths(r,e)),n.output||(n=kl(r,e)),de.compileRe(n,e,t,i)};de.toRegex=(r,e)=>{try{let t=e||{};return new RegExp(r,t.flags||(t.nocase?"i":""))}catch(t){if(e&&e.debug===!0)throw t;return/$^/}};de.constants=l2;ng.exports=de});var og=x((p6,ag)=>{u();"use strict";ag.exports=sg()});var dg=x((d6,pg)=>{u();"use strict";var lg=(Fn(),Bn),ug=jm(),ot=og(),Al=qi(),fg=r=>r===""||r==="./",cg=r=>{let e=r.indexOf("{");return e>-1&&r.indexOf("}",e)>-1},oe=(r,e,t)=>{e=[].concat(e),r=[].concat(r);let i=new Set,n=new Set,s=new Set,a=0,o=f=>{s.add(f.output),t&&t.onResult&&t.onResult(f)};for(let f=0;f!i.has(f));if(t&&c.length===0){if(t.failglob===!0)throw new Error(`No matches found for "${e.join(", ")}"`);if(t.nonull===!0||t.nullglob===!0)return t.unescape?e.map(f=>f.replace(/\\/g,"")):e}return c};oe.match=oe;oe.matcher=(r,e)=>ot(r,e);oe.isMatch=(r,e,t)=>ot(e,t)(r);oe.any=oe.isMatch;oe.not=(r,e,t={})=>{e=[].concat(e).map(String);let i=new Set,n=[],s=o=>{t.onResult&&t.onResult(o),n.push(o.output)},a=new Set(oe(r,e,{...t,onResult:s}));for(let o of n)a.has(o)||i.add(o);return[...i]};oe.contains=(r,e,t)=>{if(typeof r!="string")throw new TypeError(`Expected a string: "${lg.inspect(r)}"`);if(Array.isArray(e))return e.some(i=>oe.contains(r,i,t));if(typeof e=="string"){if(fg(r)||fg(e))return!1;if(r.includes(e)||r.startsWith("./")&&r.slice(2).includes(e))return!0}return oe.isMatch(r,e,{...t,contains:!0})};oe.matchKeys=(r,e,t)=>{if(!Al.isObject(r))throw new TypeError("Expected the first argument to be an object");let i=oe(Object.keys(r),e,t),n={};for(let s of i)n[s]=r[s];return n};oe.some=(r,e,t)=>{let i=[].concat(r);for(let n of[].concat(e)){let s=ot(String(n),t);if(i.some(a=>s(a)))return!0}return!1};oe.every=(r,e,t)=>{let i=[].concat(r);for(let n of[].concat(e)){let s=ot(String(n),t);if(!i.every(a=>s(a)))return!1}return!0};oe.all=(r,e,t)=>{if(typeof r!="string")throw new TypeError(`Expected a string: "${lg.inspect(r)}"`);return[].concat(e).every(i=>ot(i,t)(r))};oe.capture=(r,e,t)=>{let i=Al.isWindows(t),s=ot.makeRe(String(r),{...t,capture:!0}).exec(i?Al.toPosixSlashes(e):e);if(s)return s.slice(1).map(a=>a===void 0?"":a)};oe.makeRe=(...r)=>ot.makeRe(...r);oe.scan=(...r)=>ot.scan(...r);oe.parse=(r,e)=>{let t=[];for(let i of[].concat(r||[]))for(let n of ug(String(i),e))t.push(ot.parse(n,e));return t};oe.braces=(r,e)=>{if(typeof r!="string")throw new TypeError("Expected a string");return e&&e.nobrace===!0||!cg(r)?[r]:ug(r,e)};oe.braceExpand=(r,e)=>{if(typeof r!="string")throw new TypeError("Expected a string");return oe.braces(r,{...e,expand:!0})};oe.hasBraces=cg;pg.exports=oe});function mg(r,e){let t=e.content.files;t=t.filter(o=>typeof o=="string"),t=t.map(ll);let i=ps.generateTasks(t),n=[],s=[];for(let o of i)n.push(...o.positive.map(l=>gg(l,!1))),s.push(...o.negative.map(l=>gg(l,!0)));let a=[...n,...s];return a=c2(r,a),a=a.flatMap(p2),a=a.map(f2),a}function gg(r,e){let t={original:r,base:r,ignore:e,pattern:r,glob:null};return Zh(r)&&Object.assign(t,nm(r)),t}function f2(r){let e=ll(r.base);return e=ps.escapePath(e),r.pattern=r.glob?`${e}/${r.glob}`:e,r.pattern=r.ignore?`!${r.pattern}`:r.pattern,r}function c2(r,e){let t=[];return r.userConfigPath&&r.tailwindConfig.content.relative&&(t=[me.dirname(r.userConfigPath)]),e.map(i=>(i.base=me.resolve(...t,i.base),i))}function p2(r){let e=[r];try{let t=be.realpathSync(r.base);t!==r.base&&e.push({...r,base:t})}catch{}return e}function yg(r,e,t){let i=r.tailwindConfig.content.files.filter(a=>typeof a.raw=="string").map(({raw:a,extension:o="html"})=>({content:a,extension:o})),[n,s]=h2(e,t);for(let a of n){let o=me.extname(a).slice(1);i.push({file:a,extension:o})}return[i,s]}function d2(r){if(!r.some(s=>s.includes("**")&&!wg.test(s)))return()=>{};let t=[],i=[];for(let s of r){let a=hg.default.matcher(s);wg.test(s)&&i.push(a),t.push(a)}let n=!1;return s=>{if(n||i.some(f=>f(s)))return;let a=t.findIndex(f=>f(s));if(a===-1)return;let o=r[a],l=me.relative(m.cwd(),o);l[0]!=="."&&(l=`./${l}`);let c=bg.find(f=>s.includes(f));c&&(n=!0,G.warn("broad-content-glob-pattern",[`Your \`content\` configuration includes a pattern which looks like it's accidentally matching all of \`${c}\` and can cause serious performance issues.`,`Pattern: \`${l}\``,"See our documentation for recommendations:","https://tailwindcss.com/docs/content-configuration#pattern-recommendations"]))}}function h2(r,e){let t=r.map(o=>o.pattern),i=new Map,n=d2(t),s=new Set;Ze.DEBUG&&console.time("Finding changed files");let a=ps.sync(t,{absolute:!0});for(let o of a){n(o);let l=e.get(o)||-1/0,c=be.statSync(o).mtimeMs;c>l&&(s.add(o),i.set(o,c))}return Ze.DEBUG&&console.timeEnd("Finding changed files"),[s,i]}var hg,bg,wg,vg=P(()=>{u();ft();et();Jh();em();tm();sm();It();Be();hg=pe(dg());bg=["node_modules"],wg=new RegExp(`(${bg.map(r=>String.raw`\b${r}\b`).join("|")})`)});function xg(){}var kg=P(()=>{u()});function b2(r,e){for(let t of e){let i=`${r}${t}`;if(be.existsSync(i)&&be.statSync(i).isFile())return i}for(let t of e){let i=`${r}/index${t}`;if(be.existsSync(i))return i}return null}function*Sg(r,e,t,i=me.extname(r)){let n=b2(me.resolve(e,r),m2.includes(i)?g2:y2);if(n===null||t.has(n))return;t.add(n),yield n,e=me.dirname(n),i=me.extname(n);let s=be.readFileSync(n,"utf-8");for(let a of[...s.matchAll(/import[\s\S]*?['"](.{3,}?)['"]/gi),...s.matchAll(/import[\s\S]*from[\s\S]*?['"](.{3,}?)['"]/gi),...s.matchAll(/require\(['"`](.+)['"`]\)/gi)])!a[1].startsWith(".")||(yield*Sg(a[1],e,t,i))}function Cl(r){return r===null?new Set:new Set(Sg(r,me.dirname(r),new Set))}var m2,g2,y2,Ag=P(()=>{u();ft();et();m2=[".js",".cjs",".mjs"],g2=["",".js",".cjs",".mjs",".ts",".cts",".mts",".jsx",".tsx"],y2=["",".ts",".cts",".mts",".tsx",".js",".cjs",".mjs",".jsx"]});function w2(r,e){if(_l.has(r))return _l.get(r);let t=mg(r,e);return _l.set(r,t).get(r)}function v2(r){let e=aa(r);if(e!==null){let[i,n,s,a]=_g.get(e)||[],o=Cl(e),l=!1,c=new Map;for(let p of o){let h=be.statSync(p).mtimeMs;c.set(p,h),(!a||!a.has(p)||h>a.get(p))&&(l=!0)}if(!l)return[i,e,n,s];for(let p of o)delete hf.cache[p];let f=ol(zr(xg(e))),d=Wi(f);return _g.set(e,[f,d,o,c]),[f,e,d,o]}let t=zr(r?.config??r??{});return t=ol(t),[t,null,Wi(t),[]]}function El(r){return({tailwindDirectives:e,registerDependency:t})=>(i,n)=>{let[s,a,o,l]=v2(r),c=new Set(l);if(e.size>0){c.add(n.opts.from);for(let b of n.messages)b.type==="dependency"&&c.add(b.file)}let[f,,d]=Vh(i,n,s,a,o,c),p=cs(f),h=w2(f,s);if(e.size>0){for(let y of h)for(let w of nl(y))t(w);let[b,v]=yg(f,h,p);for(let y of b)f.changedContent.push(y);for(let[y,w]of v.entries())d.set(y,w)}for(let b of l)t({type:"dependency",file:b});for(let[b,v]of d.entries())p.set(b,v);return f}}var Cg,_g,_l,Eg=P(()=>{u();ft();Cg=pe(Fs());wf();sa();oc();Oi();Hh();Xh();vg();kg();Ag();_g=new Cg.default({maxSize:100}),_l=new WeakMap});function Ol(r){let e=new Set,t=new Set,i=new Set;if(r.walkAtRules(n=>{n.name==="apply"&&i.add(n),n.name==="import"&&(n.params==='"tailwindcss/base"'||n.params==="'tailwindcss/base'"?(n.name="tailwind",n.params="base"):n.params==='"tailwindcss/components"'||n.params==="'tailwindcss/components'"?(n.name="tailwind",n.params="components"):n.params==='"tailwindcss/utilities"'||n.params==="'tailwindcss/utilities'"?(n.name="tailwind",n.params="utilities"):(n.params==='"tailwindcss/screens"'||n.params==="'tailwindcss/screens'"||n.params==='"tailwindcss/variants"'||n.params==="'tailwindcss/variants'")&&(n.name="tailwind",n.params="variants")),n.name==="tailwind"&&(n.params==="screens"&&(n.params="variants"),e.add(n.params)),["layer","responsive","variants"].includes(n.name)&&(["responsive","variants"].includes(n.name)&&G.warn(`${n.name}-at-rule-deprecated`,[`The \`@${n.name}\` directive has been deprecated in Tailwind CSS v3.0.`,"Use `@layer utilities` or `@layer components` instead.","https://tailwindcss.com/docs/upgrade-guide#replace-variants-with-layer"]),t.add(n))}),!e.has("base")||!e.has("components")||!e.has("utilities")){for(let n of t)if(n.name==="layer"&&["base","components","utilities"].includes(n.params)){if(!e.has(n.params))throw n.error(`\`@layer ${n.params}\` is used but no matching \`@tailwind ${n.params}\` directive is present.`)}else if(n.name==="responsive"){if(!e.has("utilities"))throw n.error("`@responsive` is used but `@tailwind utilities` is missing.")}else if(n.name==="variants"&&!e.has("utilities"))throw n.error("`@variants` is used but `@tailwind utilities` is missing.")}return{tailwindDirectives:e,applyDirectives:i}}var Og=P(()=>{u();Be()});function Qt(r,e=void 0,t=void 0){return r.map(i=>{let n=i.clone();return t!==void 0&&(n.raws.tailwind={...n.raws.tailwind,...t}),e!==void 0&&Tg(n,s=>{if(s.raws.tailwind?.preserveSource===!0&&s.source)return!1;s.source=e}),n})}function Tg(r,e){e(r)!==!1&&r.each?.(t=>Tg(t,e))}var Rg=P(()=>{u()});function Tl(r){return r=Array.isArray(r)?r:[r],r=r.map(e=>e instanceof RegExp?e.source:e),r.join("")}function Ne(r){return new RegExp(Tl(r),"g")}function qt(r){return`(?:${r.map(Tl).join("|")})`}function Rl(r){return`(?:${Tl(r)})?`}function Ig(r){return r&&x2.test(r)?r.replace(Pg,"\\$&"):r||""}var Pg,x2,Dg=P(()=>{u();Pg=/[\\^$.*+?()[\]{}|]/g,x2=RegExp(Pg.source)});function qg(r){let e=Array.from(k2(r));return t=>{let i=[];for(let n of e)for(let s of t.match(n)??[])i.push(C2(s));for(let n of i.slice()){let s=ve(n,".");for(let a=0;a=s.length-1){i.push(o);continue}let l=Number(s[a+1]);isNaN(l)?i.push(o):a++}}return i}}function*k2(r){let e=r.tailwindConfig.separator,t=r.tailwindConfig.prefix!==""?Rl(Ne([/-?/,Ig(r.tailwindConfig.prefix)])):"",i=qt([/\[[^\s:'"`]+:[^\s\[\]]+\]/,/\[[^\s:'"`\]]+:[^\s]+?\[[^\s]+\][^\s]+?\]/,Ne([qt([/-?(?:\w+)/,/@(?:\w+)/]),Rl(qt([Ne([qt([/-(?:\w+-)*\['[^\s]+'\]/,/-(?:\w+-)*\["[^\s]+"\]/,/-(?:\w+-)*\[`[^\s]+`\]/,/-(?:\w+-)*\[(?:[^\s\[\]]+\[[^\s\[\]]+\])*[^\s:\[\]]+\]/]),/(?![{([]])/,/(?:\/[^\s'"`\\><$]*)?/]),Ne([qt([/-(?:\w+-)*\['[^\s]+'\]/,/-(?:\w+-)*\["[^\s]+"\]/,/-(?:\w+-)*\[`[^\s]+`\]/,/-(?:\w+-)*\[(?:[^\s\[\]]+\[[^\s\[\]]+\])*[^\s\[\]]+\]/]),/(?![{([]])/,/(?:\/[^\s'"`\\$]*)?/]),/[-\/][^\s'"`\\$={><]*/]))])]),n=[qt([Ne([/@\[[^\s"'`]+\](\/[^\s"'`]+)?/,e]),Ne([/([^\s"'`\[\\]+-)?\[[^\s"'`]+\]\/[\w_-]+/,e]),Ne([/([^\s"'`\[\\]+-)?\[[^\s"'`]+\]/,e]),Ne([/[^\s"'`\[\\]+/,e])]),qt([Ne([/([^\s"'`\[\\]+-)?\[[^\s`]+\]\/[\w_-]+/,e]),Ne([/([^\s"'`\[\\]+-)?\[[^\s`]+\]/,e]),Ne([/[^\s`\[\\]+/,e])])];for(let s of n)yield Ne(["((?=((",s,")+))\\2)?",/!?/,t,i]);yield/[^<>"'`\s.(){}[\]#=%$][^<>"'`\s(){}[\]#=%$]*[^<>"'`\s.(){}[\]#=%:$]/g}function C2(r){if(!r.includes("-["))return r;let e=0,t=[],i=r.matchAll(S2);i=Array.from(i).flatMap(n=>{let[,...s]=n;return s.map((a,o)=>Object.assign([],n,{index:n.index+o,0:a}))});for(let n of i){let s=n[0],a=t[t.length-1];if(s===a?t.pop():(s==="'"||s==='"'||s==="`")&&t.push(s),!a){if(s==="["){e++;continue}else if(s==="]"){e--;continue}if(e<0)return r.substring(0,n.index-1);if(e===0&&!A2.test(s))return r.substring(0,n.index)}}return r}var S2,A2,$g=P(()=>{u();Dg();zt();S2=/([\[\]'"`])([^\[\]'"`])?/g,A2=/[^"'`\s<>\]]+/});function _2(r,e){let t=r.tailwindConfig.content.extract;return t[e]||t.DEFAULT||Mg[e]||Mg.DEFAULT(r)}function E2(r,e){let t=r.content.transform;return t[e]||t.DEFAULT||Ng[e]||Ng.DEFAULT}function O2(r,e,t,i){Li.has(e)||Li.set(e,new Lg.default({maxSize:25e3}));for(let n of r.split(` +`))if(n=n.trim(),!i.has(n))if(i.add(n),Li.get(e).has(n))for(let s of Li.get(e).get(n))t.add(s);else{let s=e(n).filter(o=>o!=="!*"),a=new Set(s);for(let o of a)t.add(o);Li.get(e).set(n,a)}}function T2(r,e){let t=e.offsets.sort(r),i={base:new Set,defaults:new Set,components:new Set,utilities:new Set,variants:new Set};for(let[n,s]of t)i[n.layer].add(s);return i}function Pl(r){return async e=>{let t={base:null,components:null,utilities:null,variants:null};if(e.walkAtRules(y=>{y.name==="tailwind"&&Object.keys(t).includes(y.params)&&(t[y.params]=y)}),Object.values(t).every(y=>y===null))return e;let i=new Set([...r.candidates??[],gt]),n=new Set;bt.DEBUG&&console.time("Reading changed files");let s=[];for(let y of r.changedContent){let w=E2(r.tailwindConfig,y.extension),k=_2(r,y.extension);s.push([y,{transformer:w,extractor:k}])}let a=500;for(let y=0;y{S=k?await be.promises.readFile(k,"utf8"):S,O2(E(S),T,i,n)}))}bt.DEBUG&&console.timeEnd("Reading changed files");let o=r.classCache.size;bt.DEBUG&&console.time("Generate rules"),bt.DEBUG&&console.time("Sorting candidates");let l=new Set([...i].sort((y,w)=>y===w?0:y{let w=y.raws.tailwind?.parentLayer;return w==="components"?t.components!==null:w==="utilities"?t.utilities!==null:!0});t.variants?(t.variants.before(Qt(b,t.variants.source,{layer:"variants"})),t.variants.remove()):b.length>0&&e.append(Qt(b,e.source,{layer:"variants"})),e.source.end=e.source.end??e.source.start;let v=b.some(y=>y.raws.tailwind?.parentLayer==="utilities");t.utilities&&p.size===0&&!v&&G.warn("content-problems",["No utility classes were detected in your source files. If this is unexpected, double-check the `content` option in your Tailwind CSS configuration.","https://tailwindcss.com/docs/content-configuration"]),bt.DEBUG&&(console.log("Potential classes: ",i.size),console.log("Active contexts: ",es.size)),r.changedContent=[],e.walkAtRules("layer",y=>{Object.keys(t).includes(y.params)&&y.remove()})}}var Lg,bt,Mg,Ng,Li,Bg=P(()=>{u();ft();Lg=pe(Fs());It();os();Be();Rg();$g();bt=Ze,Mg={DEFAULT:qg},Ng={DEFAULT:r=>r,svelte:r=>r.replace(/(?:^|\s)class:/g," ")};Li=new WeakMap});function xs(r){let e=new Map;ee.root({nodes:[r.clone()]}).walkRules(s=>{(0,vs.default)(a=>{a.walkClasses(o=>{let l=o.parent.toString(),c=e.get(l);c||e.set(l,c=new Set),c.add(o.value)})}).processSync(s.selector)});let i=Array.from(e.values(),s=>Array.from(s)),n=i.flat();return Object.assign(n,{groups:i})}function Il(r){return R2.astSync(r)}function Fg(r,e){let t=new Set;for(let i of r)t.add(i.split(e).pop());return Array.from(t)}function jg(r,e){let t=r.tailwindConfig.prefix;return typeof t=="function"?t(e):t+e}function*zg(r){for(yield r;r.parent;)yield r.parent,r=r.parent}function P2(r,e={}){let t=r.nodes;r.nodes=[];let i=r.clone(e);return r.nodes=t,i}function I2(r){for(let e of zg(r))if(r!==e){if(e.type==="root")break;r=P2(e,{nodes:[r]})}return r}function D2(r,e){let t=new Map;return r.walkRules(i=>{for(let a of zg(i))if(a.raws.tailwind?.layer!==void 0)return;let n=I2(i),s=e.offsets.create("user");for(let a of xs(i)){let o=t.get(a)||[];t.set(a,o),o.push([{layer:"user",sort:s,important:!1},n])}}),t}function q2(r,e){for(let t of r){if(e.notClassCache.has(t)||e.applyClassCache.has(t))continue;if(e.classCache.has(t)){e.applyClassCache.set(t,e.classCache.get(t).map(([n,s])=>[n,s.clone()]));continue}let i=Array.from(Yo(t,e));if(i.length===0){e.notClassCache.add(t);continue}e.applyClassCache.set(t,i)}return e.applyClassCache}function $2(r){let e=null;return{get:t=>(e=e||r(),e.get(t)),has:t=>(e=e||r(),e.has(t))}}function L2(r){return{get:e=>r.flatMap(t=>t.get(e)||[]),has:e=>r.some(t=>t.has(e))}}function Ug(r){let e=r.split(/[\s\t\n]+/g);return e[e.length-1]==="!important"?[e.slice(0,-1),!0]:[e,!1]}function Vg(r,e,t){let i=new Set,n=[];if(r.walkAtRules("apply",l=>{let[c]=Ug(l.params);for(let f of c)i.add(f);n.push(l)}),n.length===0)return;let s=L2([t,q2(i,e)]);function a(l,c,f){let d=Il(l),p=Il(c),b=Il(`.${Te(f)}`).nodes[0].nodes[0];return d.each(v=>{let y=new Set;p.each(w=>{let k=!1;w=w.clone(),w.walkClasses(S=>{S.value===b.value&&(k||(S.replaceWith(...v.nodes.map(E=>E.clone())),y.add(w),k=!0))})});for(let w of y){let k=[[]];for(let S of w.nodes)S.type==="combinator"?(k.push(S),k.push([])):k[k.length-1].push(S);w.nodes=[];for(let S of k)Array.isArray(S)&&S.sort((E,T)=>E.type==="tag"&&T.type==="class"?-1:E.type==="class"&&T.type==="tag"?1:E.type==="class"&&T.type==="pseudo"&&T.value.startsWith("::")?-1:E.type==="pseudo"&&E.value.startsWith("::")&&T.type==="class"?1:0),w.nodes=w.nodes.concat(S)}v.replaceWith(...y)}),d.toString()}let o=new Map;for(let l of n){let[c]=o.get(l.parent)||[[],l.source];o.set(l.parent,[c,l.source]);let[f,d]=Ug(l.params);if(l.parent.type==="atrule"){if(l.parent.name==="screen"){let p=l.parent.params;throw l.error(`@apply is not supported within nested at-rules like @screen. We suggest you write this as @apply ${f.map(h=>`${p}:${h}`).join(" ")} instead.`)}throw l.error(`@apply is not supported within nested at-rules like @${l.parent.name}. You can fix this by un-nesting @${l.parent.name}.`)}for(let p of f){if([jg(e,"group"),jg(e,"peer")].includes(p))throw l.error(`@apply should not be used with the '${p}' utility`);if(!s.has(p))throw l.error(`The \`${p}\` class does not exist. If \`${p}\` is a custom class, make sure it is defined within a \`@layer\` directive.`);let h=s.get(p);for(let[,b]of h)b.type!=="atrule"&&b.walkRules(()=>{throw l.error([`The \`${p}\` class cannot be used with \`@apply\` because \`@apply\` does not currently support nested CSS.`,"Rewrite the selector without nesting or configure the `tailwindcss/nesting` plugin:","https://tailwindcss.com/docs/using-with-preprocessors#nesting"].join(` +`))});c.push([p,d,h])}}for(let[l,[c,f]]of o){let d=[];for(let[h,b,v]of c){let y=[h,...Fg([h],e.tailwindConfig.separator)];for(let[w,k]of v){let S=xs(l),E=xs(k);if(E=E.groups.filter(R=>R.some(F=>y.includes(F))).flat(),E=E.concat(Fg(E,e.tailwindConfig.separator)),S.some(R=>E.includes(R)))throw k.error(`You cannot \`@apply\` the \`${h}\` utility here because it creates a circular dependency.`);let B=ee.root({nodes:[k.clone()]});B.walk(R=>{R.source=f}),(k.type!=="atrule"||k.type==="atrule"&&k.name!=="keyframes")&&B.walkRules(R=>{if(!xs(R).some(U=>U===h)){R.remove();return}let F=typeof e.tailwindConfig.important=="string"?e.tailwindConfig.important:null,_=l.raws.tailwind!==void 0&&F&&l.selector.indexOf(F)===0?l.selector.slice(F.length):l.selector;_===""&&(_=l.selector),R.selector=a(_,R.selector,h),F&&_!==l.selector&&(R.selector=is(R.selector,F)),R.walkDecls(U=>{U.important=w.important||b});let Q=(0,vs.default)().astSync(R.selector);Q.each(U=>pr(U)),R.selector=Q.toString()}),!!B.nodes[0]&&d.push([w.sort,B.nodes[0]])}}let p=e.offsets.sort(d).map(h=>h[1]);l.after(p)}for(let l of n)l.parent.nodes.length>1?l.remove():l.parent.remove();Vg(r,e,t)}function Dl(r){return e=>{let t=$2(()=>D2(e,r));Vg(e,r,t)}}var vs,R2,Hg=P(()=>{u();Ot();vs=pe(it());os();fr();Wo();ts();R2=(0,vs.default)()});var Wg=x((nq,ks)=>{u();(function(){"use strict";function r(i,n,s){if(!i)return null;r.caseSensitive||(i=i.toLowerCase());var a=r.threshold===null?null:r.threshold*i.length,o=r.thresholdAbsolute,l;a!==null&&o!==null?l=Math.min(a,o):a!==null?l=a:o!==null?l=o:l=null;var c,f,d,p,h,b=n.length;for(h=0;hs)return s+1;var l=[],c,f,d,p,h;for(c=0;c<=o;c++)l[c]=[c];for(f=0;f<=a;f++)l[0][f]=f;for(c=1;c<=o;c++){for(d=e,p=1,c>s&&(p=c-s),h=o+1,h>s+c&&(h=s+c),f=1;f<=a;f++)fh?l[c][f]=s+1:n.charAt(c-1)===i.charAt(f-1)?l[c][f]=l[c-1][f-1]:l[c][f]=Math.min(l[c-1][f-1]+1,Math.min(l[c][f-1]+1,l[c-1][f]+1)),l[c][f]s)return s+1}return l[o][a]}})()});var Qg=x((sq,Gg)=>{u();var ql="(".charCodeAt(0),$l=")".charCodeAt(0),Ss="'".charCodeAt(0),Ll='"'.charCodeAt(0),Ml="\\".charCodeAt(0),yr="/".charCodeAt(0),Nl=",".charCodeAt(0),Bl=":".charCodeAt(0),As="*".charCodeAt(0),M2="u".charCodeAt(0),N2="U".charCodeAt(0),B2="+".charCodeAt(0),F2=/^[a-f0-9?-]+$/i;Gg.exports=function(r){for(var e=[],t=r,i,n,s,a,o,l,c,f,d=0,p=t.charCodeAt(d),h=t.length,b=[{nodes:e}],v=0,y,w="",k="",S="";d{u();Yg.exports=function r(e,t,i){var n,s,a,o;for(n=0,s=e.length;n{u();function Xg(r,e){var t=r.type,i=r.value,n,s;return e&&(s=e(r))!==void 0?s:t==="word"||t==="space"?i:t==="string"?(n=r.quote||"",n+i+(r.unclosed?"":n)):t==="comment"?"/*"+i+(r.unclosed?"":"*/"):t==="div"?(r.before||"")+i+(r.after||""):Array.isArray(r.nodes)?(n=Zg(r.nodes,e),t!=="function"?n:i+"("+(r.before||"")+n+(r.after||"")+(r.unclosed?"":")")):i}function Zg(r,e){var t,i;if(Array.isArray(r)){for(t="",i=r.length-1;~i;i-=1)t=Xg(r[i],e)+t;return t}return Xg(r,e)}Jg.exports=Zg});var ry=x((lq,ty)=>{u();var Cs="-".charCodeAt(0),_s="+".charCodeAt(0),Fl=".".charCodeAt(0),j2="e".charCodeAt(0),z2="E".charCodeAt(0);function U2(r){var e=r.charCodeAt(0),t;if(e===_s||e===Cs){if(t=r.charCodeAt(1),t>=48&&t<=57)return!0;var i=r.charCodeAt(2);return t===Fl&&i>=48&&i<=57}return e===Fl?(t=r.charCodeAt(1),t>=48&&t<=57):e>=48&&e<=57}ty.exports=function(r){var e=0,t=r.length,i,n,s;if(t===0||!U2(r))return!1;for(i=r.charCodeAt(e),(i===_s||i===Cs)&&e++;e57));)e+=1;if(i=r.charCodeAt(e),n=r.charCodeAt(e+1),i===Fl&&n>=48&&n<=57)for(e+=2;e57));)e+=1;if(i=r.charCodeAt(e),n=r.charCodeAt(e+1),s=r.charCodeAt(e+2),(i===j2||i===z2)&&(n>=48&&n<=57||(n===_s||n===Cs)&&s>=48&&s<=57))for(e+=n===_s||n===Cs?3:2;e57));)e+=1;return{number:r.slice(0,e),unit:r.slice(e)}}});var ay=x((uq,sy)=>{u();var V2=Qg(),iy=Kg(),ny=ey();function $t(r){return this instanceof $t?(this.nodes=V2(r),this):new $t(r)}$t.prototype.toString=function(){return Array.isArray(this.nodes)?ny(this.nodes):""};$t.prototype.walk=function(r,e){return iy(this.nodes,r,e),this};$t.unit=ry();$t.walk=iy;$t.stringify=ny;sy.exports=$t});function zl(r){return typeof r=="object"&&r!==null}function H2(r,e){let t=kt(e);do if(t.pop(),(0,Mi.default)(r,t)!==void 0)break;while(t.length);return t.length?t:void 0}function br(r){return typeof r=="string"?r:r.reduce((e,t,i)=>t.includes(".")?`${e}[${t}]`:i===0?t:`${e}.${t}`,"")}function ly(r){return r.map(e=>`'${e}'`).join(", ")}function uy(r){return ly(Object.keys(r))}function Ul(r,e,t,i={}){let n=Array.isArray(e)?br(e):e.replace(/^['"]+|['"]+$/g,""),s=Array.isArray(e)?e:kt(n),a=(0,Mi.default)(r.theme,s,t);if(a===void 0){let l=`'${n}' does not exist in your theme config.`,c=s.slice(0,-1),f=(0,Mi.default)(r.theme,c);if(zl(f)){let d=Object.keys(f).filter(h=>Ul(r,[...c,h]).isValid),p=(0,oy.default)(s[s.length-1],d);p?l+=` Did you mean '${br([...c,p])}'?`:d.length>0&&(l+=` '${br(c)}' has the following valid keys: ${ly(d)}`)}else{let d=H2(r.theme,n);if(d){let p=(0,Mi.default)(r.theme,d);zl(p)?l+=` '${br(d)}' has the following keys: ${uy(p)}`:l+=` '${br(d)}' is not an object.`}else l+=` Your theme has the following top-level keys: ${uy(r.theme)}`}return{isValid:!1,error:l}}if(!(typeof a=="string"||typeof a=="number"||typeof a=="function"||a instanceof String||a instanceof Number||Array.isArray(a))){let l=`'${n}' was found but does not resolve to a string.`;if(zl(a)){let c=Object.keys(a).filter(f=>Ul(r,[...s,f]).isValid);c.length&&(l+=` Did you mean something like '${br([...s,c[0]])}'?`)}return{isValid:!1,error:l}}let[o]=s;return{isValid:!0,value:mt(o)(a,i)}}function W2(r,e,t){e=e.map(n=>fy(r,n,t));let i=[""];for(let n of e)n.type==="div"&&n.value===","?i.push(""):i[i.length-1]+=jl.default.stringify(n);return i}function fy(r,e,t){if(e.type==="function"&&t[e.value]!==void 0){let i=W2(r,e.nodes,t);e.type="word",e.value=t[e.value](r,...i)}return e}function G2(r,e,t){return Object.keys(t).some(n=>e.includes(`${n}(`))?(0,jl.default)(e).walk(n=>{fy(r,n,t)}).toString():e}function*Y2(r){r=r.replace(/^['"]+|['"]+$/g,"");let e=r.match(/^([^\s]+)(?![^\[]*\])(?:\s*\/\s*([^\/\s]+))$/),t;yield[r,void 0],e&&(r=e[1],t=e[2],yield[r,t])}function K2(r,e,t){let i=Array.from(Y2(e)).map(([n,s])=>Object.assign(Ul(r,n,t,{opacityValue:s}),{resolvedPath:n,alpha:s}));return i.find(n=>n.isValid)??i[0]}function cy(r){let e=r.tailwindConfig,t={theme:(i,n,...s)=>{let{isValid:a,value:o,error:l,alpha:c}=K2(e,n,s.length?s:void 0);if(!a){let p=i.parent,h=p?.raws.tailwind?.candidate;if(p&&h!==void 0){r.markInvalidUtilityNode(p),p.remove(),G.warn("invalid-theme-key-in-class",[`The utility \`${h}\` contains an invalid theme value and was not generated.`]);return}throw i.error(l)}let f=Xt(o),d=f!==void 0&&typeof f=="function";return(c!==void 0||d)&&(c===void 0&&(c=1),o=Je(f,c,f)),o},screen:(i,n)=>{n=n.replace(/^['"]+/g,"").replace(/['"]+$/g,"");let a=Rt(e.theme.screens).find(({name:o})=>o===n);if(!a)throw i.error(`The '${n}' screen does not exist in your theme.`);return Tt(a)}};return i=>{i.walk(n=>{let s=Q2[n.type];s!==void 0&&(n[s]=G2(n,n[s],t))})}}var Mi,oy,jl,Q2,py=P(()=>{u();Mi=pe(Ra()),oy=pe(Wg());Ci();jl=pe(ay());Zn();Yn();Yi();Lr();Fr();Be();Q2={atrule:"params",decl:"value"}});function dy({tailwindConfig:{theme:r}}){return function(e){e.walkAtRules("screen",t=>{let i=t.params,s=Rt(r.screens).find(({name:a})=>a===i);if(!s)throw t.error(`No \`${i}\` screen found.`);t.name="media",t.params=Tt(s)})}}var hy=P(()=>{u();Zn();Yn()});function X2(r){let e=r.filter(o=>o.type!=="pseudo"||o.nodes.length>0?!0:o.value.startsWith("::")||[":before",":after",":first-line",":first-letter"].includes(o.value)).reverse(),t=new Set(["tag","class","id","attribute"]),i=e.findIndex(o=>t.has(o.type));if(i===-1)return e.reverse().join("").trim();let n=e[i],s=my[n.type]?my[n.type](n):n;e=e.slice(0,i);let a=e.findIndex(o=>o.type==="combinator"&&o.value===">");return a!==-1&&(e.splice(0,a),e.unshift(Es.default.universal())),[s,...e.reverse()].join("").trim()}function J2(r){return Vl.has(r)||Vl.set(r,Z2.transformSync(r)),Vl.get(r)}function Hl({tailwindConfig:r}){return e=>{let t=new Map,i=new Set;if(e.walkAtRules("defaults",n=>{if(n.nodes&&n.nodes.length>0){i.add(n);return}let s=n.params;t.has(s)||t.set(s,new Set),t.get(s).add(n.parent),n.remove()}),we(r,"optimizeUniversalDefaults"))for(let n of i){let s=new Map,a=t.get(n.params)??[];for(let o of a)for(let l of J2(o.selector)){let c=l.includes(":-")||l.includes("::-")||l.includes(":has")?l:"__DEFAULT__",f=s.get(c)??new Set;s.set(c,f),f.add(l)}if(s.size===0){n.remove();continue}for(let[,o]of s){let l=ee.rule({source:n.source});l.selectors=[...o],l.append(n.nodes.map(c=>c.clone())),n.before(l)}n.remove()}else if(i.size){let n=ee.rule({selectors:["*","::before","::after"]});for(let a of i)n.append(a.nodes),n.parent||a.before(n),n.source||(n.source=a.source),a.remove();let s=n.clone({selectors:["::backdrop"]});n.after(s)}}}var Es,my,Z2,Vl,gy=P(()=>{u();Ot();Es=pe(it());ct();my={id(r){return Es.default.attribute({attribute:"id",operator:"=",value:r.value,quoteMark:'"'})}};Z2=(0,Es.default)(r=>r.map(e=>{let t=e.split(i=>i.type==="combinator"&&i.value===" ").pop();return X2(t)})),Vl=new Map});function Wl(){function r(e){let t=null;e.each(i=>{if(!eO.has(i.type)){t=null;return}if(t===null){t=i;return}let n=yy[i.type];i.type==="atrule"&&i.name==="font-face"?t=i:n.every(s=>(i[s]??"").replace(/\s+/g," ")===(t[s]??"").replace(/\s+/g," "))?(i.nodes&&t.append(i.nodes),i.remove()):t=i}),e.each(i=>{i.type==="atrule"&&r(i)})}return e=>{r(e)}}var yy,eO,by=P(()=>{u();yy={atrule:["name","params"],rule:["selector"]},eO=new Set(Object.keys(yy))});function Gl(){return r=>{r.walkRules(e=>{let t=new Map,i=new Set([]),n=new Map;e.walkDecls(s=>{if(s.parent===e){if(t.has(s.prop)){if(t.get(s.prop).value===s.value){i.add(t.get(s.prop)),t.set(s.prop,s);return}n.has(s.prop)||n.set(s.prop,new Set),n.get(s.prop).add(t.get(s.prop)),n.get(s.prop).add(s)}t.set(s.prop,s)}});for(let s of i)s.remove();for(let s of n.values()){let a=new Map;for(let o of s){let l=rO(o.value);l!==null&&(a.has(l)||a.set(l,new Set),a.get(l).add(o))}for(let o of a.values()){let l=Array.from(o).slice(0,-1);for(let c of l)c.remove()}}})}}function rO(r){let e=/^-?\d*.?\d+([\w%]+)?$/g.exec(r);return e?e[1]??tO:null}var tO,wy=P(()=>{u();tO=Symbol("unitless-number")});function iO(r){if(!r.walkAtRules)return;let e=new Set;if(r.walkAtRules("apply",t=>{e.add(t.parent)}),e.size!==0)for(let t of e){let i=[],n=[];for(let s of t.nodes)s.type==="atrule"&&s.name==="apply"?(n.length>0&&(i.push(n),n=[]),i.push([s])):n.push(s);if(n.length>0&&i.push(n),i.length!==1){for(let s of[...i].reverse()){let a=t.clone({nodes:[]});a.append(s),t.after(a)}t.remove()}}}function Os(){return r=>{iO(r)}}var vy=P(()=>{u()});function Ts(r){return async function(e,t){let{tailwindDirectives:i,applyDirectives:n}=Ol(e);Os()(e,t);let s=r({tailwindDirectives:i,applyDirectives:n,registerDependency(a){t.messages.push({plugin:"tailwindcss",parent:t.opts.from,...a})},createContext(a,o){return il(a,o,e)}})(e,t);if(s.tailwindConfig.separator==="-")throw new Error("The '-' character cannot be used as a custom separator in JIT mode due to parsing ambiguity. Please use another character like '_' instead.");Rf(s.tailwindConfig),await Pl(s)(e,t),Os()(e,t),Dl(s)(e,t),cy(s)(e,t),dy(s)(e,t),Hl(s)(e,t),Wl(s)(e,t),Gl(s)(e,t)}}var xy=P(()=>{u();Og();Bg();Hg();py();hy();gy();by();wy();vy();Oi();ct()});function ky(r,e){let t=null,i=null;return r.walkAtRules("config",n=>{if(i=n.source?.input.file??e.opts.from??null,i===null)throw n.error("The `@config` directive cannot be used without setting `from` in your PostCSS config.");if(t)throw n.error("Only one `@config` directive is allowed per file.");let s=n.params.match(/(['"])(.*?)\1/);if(!s)throw n.error("A path is required when using the `@config` directive.");let a=s[2];if(me.isAbsolute(a))throw n.error("The `@config` directive cannot be used with an absolute path.");if(t=me.resolve(me.dirname(i),a),!be.existsSync(t))throw n.error(`The config file at "${a}" does not exist. Make sure the path is correct and the file exists.`);n.remove()}),t||null}var Sy=P(()=>{u();ft();et()});var Ay=x((Wq,Ql)=>{u();Eg();xy();It();Sy();Ql.exports=function(e){return{postcssPlugin:"tailwindcss",plugins:[Ze.DEBUG&&function(t){return console.log(` +`),console.time("JIT TOTAL"),t},async function(t,i){e=ky(t,i)??e;let n=El(e);if(t.type==="document"){let s=t.nodes.filter(a=>a.type==="root");for(let a of s)a.type==="root"&&await Ts(n)(a,i);return}await Ts(n)(t,i)},Ze.DEBUG&&function(t){return console.timeEnd("JIT TOTAL"),console.log(` +`),t}].filter(Boolean)}};Ql.exports.postcss=!0});var _y=x((Gq,Cy)=>{u();Cy.exports=Ay()});var Yl=x((Qq,Ey)=>{u();Ey.exports=()=>["and_chr 114","and_uc 15.5","chrome 114","chrome 113","chrome 109","edge 114","firefox 114","ios_saf 16.5","ios_saf 16.4","ios_saf 16.3","ios_saf 16.1","opera 99","safari 16.5","samsung 21"]});var Rs={};Ge(Rs,{agents:()=>nO,feature:()=>sO});function sO(){return{status:"cr",title:"CSS Feature Queries",stats:{ie:{"6":"n","7":"n","8":"n","9":"n","10":"n","11":"n","5.5":"n"},edge:{"12":"y","13":"y","14":"y","15":"y","16":"y","17":"y","18":"y","79":"y","80":"y","81":"y","83":"y","84":"y","85":"y","86":"y","87":"y","88":"y","89":"y","90":"y","91":"y","92":"y","93":"y","94":"y","95":"y","96":"y","97":"y","98":"y","99":"y","100":"y","101":"y","102":"y","103":"y","104":"y","105":"y","106":"y","107":"y","108":"y","109":"y","110":"y","111":"y","112":"y","113":"y","114":"y"},firefox:{"2":"n","3":"n","4":"n","5":"n","6":"n","7":"n","8":"n","9":"n","10":"n","11":"n","12":"n","13":"n","14":"n","15":"n","16":"n","17":"n","18":"n","19":"n","20":"n","21":"n","22":"y","23":"y","24":"y","25":"y","26":"y","27":"y","28":"y","29":"y","30":"y","31":"y","32":"y","33":"y","34":"y","35":"y","36":"y","37":"y","38":"y","39":"y","40":"y","41":"y","42":"y","43":"y","44":"y","45":"y","46":"y","47":"y","48":"y","49":"y","50":"y","51":"y","52":"y","53":"y","54":"y","55":"y","56":"y","57":"y","58":"y","59":"y","60":"y","61":"y","62":"y","63":"y","64":"y","65":"y","66":"y","67":"y","68":"y","69":"y","70":"y","71":"y","72":"y","73":"y","74":"y","75":"y","76":"y","77":"y","78":"y","79":"y","80":"y","81":"y","82":"y","83":"y","84":"y","85":"y","86":"y","87":"y","88":"y","89":"y","90":"y","91":"y","92":"y","93":"y","94":"y","95":"y","96":"y","97":"y","98":"y","99":"y","100":"y","101":"y","102":"y","103":"y","104":"y","105":"y","106":"y","107":"y","108":"y","109":"y","110":"y","111":"y","112":"y","113":"y","114":"y","115":"y","116":"y","117":"y","3.5":"n","3.6":"n"},chrome:{"4":"n","5":"n","6":"n","7":"n","8":"n","9":"n","10":"n","11":"n","12":"n","13":"n","14":"n","15":"n","16":"n","17":"n","18":"n","19":"n","20":"n","21":"n","22":"n","23":"n","24":"n","25":"n","26":"n","27":"n","28":"y","29":"y","30":"y","31":"y","32":"y","33":"y","34":"y","35":"y","36":"y","37":"y","38":"y","39":"y","40":"y","41":"y","42":"y","43":"y","44":"y","45":"y","46":"y","47":"y","48":"y","49":"y","50":"y","51":"y","52":"y","53":"y","54":"y","55":"y","56":"y","57":"y","58":"y","59":"y","60":"y","61":"y","62":"y","63":"y","64":"y","65":"y","66":"y","67":"y","68":"y","69":"y","70":"y","71":"y","72":"y","73":"y","74":"y","75":"y","76":"y","77":"y","78":"y","79":"y","80":"y","81":"y","83":"y","84":"y","85":"y","86":"y","87":"y","88":"y","89":"y","90":"y","91":"y","92":"y","93":"y","94":"y","95":"y","96":"y","97":"y","98":"y","99":"y","100":"y","101":"y","102":"y","103":"y","104":"y","105":"y","106":"y","107":"y","108":"y","109":"y","110":"y","111":"y","112":"y","113":"y","114":"y","115":"y","116":"y","117":"y"},safari:{"4":"n","5":"n","6":"n","7":"n","8":"n","9":"y","10":"y","11":"y","12":"y","13":"y","14":"y","15":"y","17":"y","9.1":"y","10.1":"y","11.1":"y","12.1":"y","13.1":"y","14.1":"y","15.1":"y","15.2-15.3":"y","15.4":"y","15.5":"y","15.6":"y","16.0":"y","16.1":"y","16.2":"y","16.3":"y","16.4":"y","16.5":"y","16.6":"y",TP:"y","3.1":"n","3.2":"n","5.1":"n","6.1":"n","7.1":"n"},opera:{"9":"n","11":"n","12":"n","15":"y","16":"y","17":"y","18":"y","19":"y","20":"y","21":"y","22":"y","23":"y","24":"y","25":"y","26":"y","27":"y","28":"y","29":"y","30":"y","31":"y","32":"y","33":"y","34":"y","35":"y","36":"y","37":"y","38":"y","39":"y","40":"y","41":"y","42":"y","43":"y","44":"y","45":"y","46":"y","47":"y","48":"y","49":"y","50":"y","51":"y","52":"y","53":"y","54":"y","55":"y","56":"y","57":"y","58":"y","60":"y","62":"y","63":"y","64":"y","65":"y","66":"y","67":"y","68":"y","69":"y","70":"y","71":"y","72":"y","73":"y","74":"y","75":"y","76":"y","77":"y","78":"y","79":"y","80":"y","81":"y","82":"y","83":"y","84":"y","85":"y","86":"y","87":"y","88":"y","89":"y","90":"y","91":"y","92":"y","93":"y","94":"y","95":"y","96":"y","97":"y","98":"y","99":"y","100":"y","12.1":"y","9.5-9.6":"n","10.0-10.1":"n","10.5":"n","10.6":"n","11.1":"n","11.5":"n","11.6":"n"},ios_saf:{"8":"n","17":"y","9.0-9.2":"y","9.3":"y","10.0-10.2":"y","10.3":"y","11.0-11.2":"y","11.3-11.4":"y","12.0-12.1":"y","12.2-12.5":"y","13.0-13.1":"y","13.2":"y","13.3":"y","13.4-13.7":"y","14.0-14.4":"y","14.5-14.8":"y","15.0-15.1":"y","15.2-15.3":"y","15.4":"y","15.5":"y","15.6":"y","16.0":"y","16.1":"y","16.2":"y","16.3":"y","16.4":"y","16.5":"y","16.6":"y","3.2":"n","4.0-4.1":"n","4.2-4.3":"n","5.0-5.1":"n","6.0-6.1":"n","7.0-7.1":"n","8.1-8.4":"n"},op_mini:{all:"y"},android:{"3":"n","4":"n","114":"y","4.4":"y","4.4.3-4.4.4":"y","2.1":"n","2.2":"n","2.3":"n","4.1":"n","4.2-4.3":"n"},bb:{"7":"n","10":"n"},op_mob:{"10":"n","11":"n","12":"n","73":"y","11.1":"n","11.5":"n","12.1":"n"},and_chr:{"114":"y"},and_ff:{"115":"y"},ie_mob:{"10":"n","11":"n"},and_uc:{"15.5":"y"},samsung:{"4":"y","20":"y","21":"y","5.0-5.4":"y","6.2-6.4":"y","7.2-7.4":"y","8.2":"y","9.2":"y","10.1":"y","11.1-11.2":"y","12.0":"y","13.0":"y","14.0":"y","15.0":"y","16.0":"y","17.0":"y","18.0":"y","19.0":"y"},and_qq:{"13.1":"y"},baidu:{"13.18":"y"},kaios:{"2.5":"y","3.0-3.1":"y"}}}}var nO,Ps=P(()=>{u();nO={ie:{prefix:"ms"},edge:{prefix:"webkit",prefix_exceptions:{"12":"ms","13":"ms","14":"ms","15":"ms","16":"ms","17":"ms","18":"ms"}},firefox:{prefix:"moz"},chrome:{prefix:"webkit"},safari:{prefix:"webkit"},opera:{prefix:"webkit",prefix_exceptions:{"9":"o","11":"o","12":"o","9.5-9.6":"o","10.0-10.1":"o","10.5":"o","10.6":"o","11.1":"o","11.5":"o","11.6":"o","12.1":"o"}},ios_saf:{prefix:"webkit"},op_mini:{prefix:"o"},android:{prefix:"webkit"},bb:{prefix:"webkit"},op_mob:{prefix:"o",prefix_exceptions:{"73":"webkit"}},and_chr:{prefix:"webkit"},and_ff:{prefix:"moz"},ie_mob:{prefix:"ms"},and_uc:{prefix:"webkit",prefix_exceptions:{"15.5":"webkit"}},samsung:{prefix:"webkit"},and_qq:{prefix:"webkit"},baidu:{prefix:"webkit"},kaios:{prefix:"moz"}}});var Oy=x(()=>{u()});var _e=x((Xq,Lt)=>{u();var{list:Kl}=$e();Lt.exports.error=function(r){let e=new Error(r);throw e.autoprefixer=!0,e};Lt.exports.uniq=function(r){return[...new Set(r)]};Lt.exports.removeNote=function(r){return r.includes(" ")?r.split(" ")[0]:r};Lt.exports.escapeRegexp=function(r){return r.replace(/[$()*+-.?[\\\]^{|}]/g,"\\$&")};Lt.exports.regexp=function(r,e=!0){return e&&(r=this.escapeRegexp(r)),new RegExp(`(^|[\\s,(])(${r}($|[\\s(,]))`,"gi")};Lt.exports.editList=function(r,e){let t=Kl.comma(r),i=e(t,[]);if(t===i)return r;let n=r.match(/,\s*/);return n=n?n[0]:", ",i.join(n)};Lt.exports.splitSelector=function(r){return Kl.comma(r).map(e=>Kl.space(e).map(t=>t.split(/(?=\.|#)/g)))}});var Mt=x((Zq,Py)=>{u();var aO=Yl(),Ty=(Ps(),Rs).agents,oO=_e(),Ry=class{static prefixes(){if(this.prefixesCache)return this.prefixesCache;this.prefixesCache=[];for(let e in Ty)this.prefixesCache.push(`-${Ty[e].prefix}-`);return this.prefixesCache=oO.uniq(this.prefixesCache).sort((e,t)=>t.length-e.length),this.prefixesCache}static withPrefix(e){return this.prefixesRegexp||(this.prefixesRegexp=new RegExp(this.prefixes().join("|"))),this.prefixesRegexp.test(e)}constructor(e,t,i,n){this.data=e,this.options=i||{},this.browserslistOpts=n||{},this.selected=this.parse(t)}parse(e){let t={};for(let i in this.browserslistOpts)t[i]=this.browserslistOpts[i];return t.path=this.options.from,aO(e,t)}prefix(e){let[t,i]=e.split(" "),n=this.data[t],s=n.prefix_exceptions&&n.prefix_exceptions[i];return s||(s=n.prefix),`-${s}-`}isSelected(e){return this.selected.includes(e)}};Py.exports=Ry});var Ni=x((Jq,Iy)=>{u();Iy.exports={prefix(r){let e=r.match(/^(-\w+-)/);return e?e[0]:""},unprefixed(r){return r.replace(/^-\w+-/,"")}}});var wr=x((e$,qy)=>{u();var lO=Mt(),Dy=Ni(),uO=_e();function Xl(r,e){let t=new r.constructor;for(let i of Object.keys(r||{})){let n=r[i];i==="parent"&&typeof n=="object"?e&&(t[i]=e):i==="source"||i===null?t[i]=n:Array.isArray(n)?t[i]=n.map(s=>Xl(s,t)):i!=="_autoprefixerPrefix"&&i!=="_autoprefixerValues"&&i!=="proxyCache"&&(typeof n=="object"&&n!==null&&(n=Xl(n,t)),t[i]=n)}return t}var Is=class{static hack(e){return this.hacks||(this.hacks={}),e.names.map(t=>(this.hacks[t]=e,this.hacks[t]))}static load(e,t,i){let n=this.hacks&&this.hacks[e];return n?new n(e,t,i):new this(e,t,i)}static clone(e,t){let i=Xl(e);for(let n in t)i[n]=t[n];return i}constructor(e,t,i){this.prefixes=t,this.name=e,this.all=i}parentPrefix(e){let t;return typeof e._autoprefixerPrefix!="undefined"?t=e._autoprefixerPrefix:e.type==="decl"&&e.prop[0]==="-"?t=Dy.prefix(e.prop):e.type==="root"?t=!1:e.type==="rule"&&e.selector.includes(":-")&&/:(-\w+-)/.test(e.selector)?t=e.selector.match(/:(-\w+-)/)[1]:e.type==="atrule"&&e.name[0]==="-"?t=Dy.prefix(e.name):t=this.parentPrefix(e.parent),lO.prefixes().includes(t)||(t=!1),e._autoprefixerPrefix=t,e._autoprefixerPrefix}process(e,t){if(!this.check(e))return;let i=this.parentPrefix(e),n=this.prefixes.filter(a=>!i||i===uO.removeNote(a)),s=[];for(let a of n)this.add(e,a,s.concat([a]),t)&&s.push(a);return s}clone(e,t){return Is.clone(e,t)}};qy.exports=Is});var j=x((t$,My)=>{u();var fO=wr(),cO=Mt(),$y=_e(),Ly=class extends fO{check(){return!0}prefixed(e,t){return t+e}normalize(e){return e}otherPrefixes(e,t){for(let i of cO.prefixes())if(i!==t&&e.includes(i))return!0;return!1}set(e,t){return e.prop=this.prefixed(e.prop,t),e}needCascade(e){return e._autoprefixerCascade||(e._autoprefixerCascade=this.all.options.cascade!==!1&&e.raw("before").includes(` +`)),e._autoprefixerCascade}maxPrefixed(e,t){if(t._autoprefixerMax)return t._autoprefixerMax;let i=0;for(let n of e)n=$y.removeNote(n),n.length>i&&(i=n.length);return t._autoprefixerMax=i,t._autoprefixerMax}calcBefore(e,t,i=""){let s=this.maxPrefixed(e,t)-$y.removeNote(i).length,a=t.raw("before");return s>0&&(a+=Array(s).fill(" ").join("")),a}restoreBefore(e){let t=e.raw("before").split(` +`),i=t[t.length-1];this.all.group(e).up(n=>{let s=n.raw("before").split(` +`),a=s[s.length-1];a.lengtha.prop===n.prop&&a.value===n.value)))return this.needCascade(e)&&(n.raws.before=this.calcBefore(i,e,t)),e.parent.insertBefore(e,n)}isAlready(e,t){let i=this.all.group(e).up(n=>n.prop===t);return i||(i=this.all.group(e).down(n=>n.prop===t)),i}add(e,t,i,n){let s=this.prefixed(e.prop,t);if(!(this.isAlready(e,s)||this.otherPrefixes(e.value,t)))return this.insert(e,t,i,n)}process(e,t){if(!this.needCascade(e)){super.process(e,t);return}let i=super.process(e,t);!i||!i.length||(this.restoreBefore(e),e.raws.before=this.calcBefore(i,e))}old(e,t){return[this.prefixed(e,t)]}};My.exports=Ly});var By=x((r$,Ny)=>{u();Ny.exports=function r(e){return{mul:t=>new r(e*t),div:t=>new r(e/t),simplify:()=>new r(e),toString:()=>e.toString()}}});var zy=x((i$,jy)=>{u();var pO=By(),dO=wr(),Zl=_e(),hO=/(min|max)-resolution\s*:\s*\d*\.?\d+(dppx|dpcm|dpi|x)/gi,mO=/(min|max)-resolution(\s*:\s*)(\d*\.?\d+)(dppx|dpcm|dpi|x)/i,Fy=class extends dO{prefixName(e,t){return e==="-moz-"?t+"--moz-device-pixel-ratio":e+t+"-device-pixel-ratio"}prefixQuery(e,t,i,n,s){return n=new pO(n),s==="dpi"?n=n.div(96):s==="dpcm"&&(n=n.mul(2.54).div(96)),n=n.simplify(),e==="-o-"&&(n=n.n+"/"+n.d),this.prefixName(e,t)+i+n}clean(e){if(!this.bad){this.bad=[];for(let t of this.prefixes)this.bad.push(this.prefixName(t,"min")),this.bad.push(this.prefixName(t,"max"))}e.params=Zl.editList(e.params,t=>t.filter(i=>this.bad.every(n=>!i.includes(n))))}process(e){let t=this.parentPrefix(e),i=t?[t]:this.prefixes;e.params=Zl.editList(e.params,(n,s)=>{for(let a of n){if(!a.includes("min-resolution")&&!a.includes("max-resolution")){s.push(a);continue}for(let o of i){let l=a.replace(hO,c=>{let f=c.match(mO);return this.prefixQuery(o,f[1],f[2],f[3],f[4])});s.push(l)}s.push(a)}return Zl.uniq(s)})}};jy.exports=Fy});var Vy=x((n$,Uy)=>{u();var Jl="(".charCodeAt(0),eu=")".charCodeAt(0),Ds="'".charCodeAt(0),tu='"'.charCodeAt(0),ru="\\".charCodeAt(0),vr="/".charCodeAt(0),iu=",".charCodeAt(0),nu=":".charCodeAt(0),qs="*".charCodeAt(0),gO="u".charCodeAt(0),yO="U".charCodeAt(0),bO="+".charCodeAt(0),wO=/^[a-f0-9?-]+$/i;Uy.exports=function(r){for(var e=[],t=r,i,n,s,a,o,l,c,f,d=0,p=t.charCodeAt(d),h=t.length,b=[{nodes:e}],v=0,y,w="",k="",S="";d{u();Hy.exports=function r(e,t,i){var n,s,a,o;for(n=0,s=e.length;n{u();function Gy(r,e){var t=r.type,i=r.value,n,s;return e&&(s=e(r))!==void 0?s:t==="word"||t==="space"?i:t==="string"?(n=r.quote||"",n+i+(r.unclosed?"":n)):t==="comment"?"/*"+i+(r.unclosed?"":"*/"):t==="div"?(r.before||"")+i+(r.after||""):Array.isArray(r.nodes)?(n=Qy(r.nodes,e),t!=="function"?n:i+"("+(r.before||"")+n+(r.after||"")+(r.unclosed?"":")")):i}function Qy(r,e){var t,i;if(Array.isArray(r)){for(t="",i=r.length-1;~i;i-=1)t=Gy(r[i],e)+t;return t}return Gy(r,e)}Yy.exports=Qy});var Zy=x((o$,Xy)=>{u();var $s="-".charCodeAt(0),Ls="+".charCodeAt(0),su=".".charCodeAt(0),vO="e".charCodeAt(0),xO="E".charCodeAt(0);function kO(r){var e=r.charCodeAt(0),t;if(e===Ls||e===$s){if(t=r.charCodeAt(1),t>=48&&t<=57)return!0;var i=r.charCodeAt(2);return t===su&&i>=48&&i<=57}return e===su?(t=r.charCodeAt(1),t>=48&&t<=57):e>=48&&e<=57}Xy.exports=function(r){var e=0,t=r.length,i,n,s;if(t===0||!kO(r))return!1;for(i=r.charCodeAt(e),(i===Ls||i===$s)&&e++;e57));)e+=1;if(i=r.charCodeAt(e),n=r.charCodeAt(e+1),i===su&&n>=48&&n<=57)for(e+=2;e57));)e+=1;if(i=r.charCodeAt(e),n=r.charCodeAt(e+1),s=r.charCodeAt(e+2),(i===vO||i===xO)&&(n>=48&&n<=57||(n===Ls||n===$s)&&s>=48&&s<=57))for(e+=n===Ls||n===$s?3:2;e57));)e+=1;return{number:r.slice(0,e),unit:r.slice(e)}}});var Ms=x((l$,tb)=>{u();var SO=Vy(),Jy=Wy(),eb=Ky();function Nt(r){return this instanceof Nt?(this.nodes=SO(r),this):new Nt(r)}Nt.prototype.toString=function(){return Array.isArray(this.nodes)?eb(this.nodes):""};Nt.prototype.walk=function(r,e){return Jy(this.nodes,r,e),this};Nt.unit=Zy();Nt.walk=Jy;Nt.stringify=eb;tb.exports=Nt});var ab=x((u$,sb)=>{u();var{list:AO}=$e(),rb=Ms(),CO=Mt(),ib=Ni(),nb=class{constructor(e){this.props=["transition","transition-property"],this.prefixes=e}add(e,t){let i,n,s=this.prefixes.add[e.prop],a=this.ruleVendorPrefixes(e),o=a||s&&s.prefixes||[],l=this.parse(e.value),c=l.map(h=>this.findProp(h)),f=[];if(c.some(h=>h[0]==="-"))return;for(let h of l){if(n=this.findProp(h),n[0]==="-")continue;let b=this.prefixes.add[n];if(!(!b||!b.prefixes))for(i of b.prefixes){if(a&&!a.some(y=>i.includes(y)))continue;let v=this.prefixes.prefixed(n,i);v!=="-ms-transform"&&!c.includes(v)&&(this.disabled(n,i)||f.push(this.clone(n,v,h)))}}l=l.concat(f);let d=this.stringify(l),p=this.stringify(this.cleanFromUnprefixed(l,"-webkit-"));if(o.includes("-webkit-")&&this.cloneBefore(e,`-webkit-${e.prop}`,p),this.cloneBefore(e,e.prop,p),o.includes("-o-")){let h=this.stringify(this.cleanFromUnprefixed(l,"-o-"));this.cloneBefore(e,`-o-${e.prop}`,h)}for(i of o)if(i!=="-webkit-"&&i!=="-o-"){let h=this.stringify(this.cleanOtherPrefixes(l,i));this.cloneBefore(e,i+e.prop,h)}d!==e.value&&!this.already(e,e.prop,d)&&(this.checkForWarning(t,e),e.cloneBefore(),e.value=d)}findProp(e){let t=e[0].value;if(/^\d/.test(t)){for(let[i,n]of e.entries())if(i!==0&&n.type==="word")return n.value}return t}already(e,t,i){return e.parent.some(n=>n.prop===t&&n.value===i)}cloneBefore(e,t,i){this.already(e,t,i)||e.cloneBefore({prop:t,value:i})}checkForWarning(e,t){if(t.prop!=="transition-property")return;let i=!1,n=!1;t.parent.each(s=>{if(s.type!=="decl"||s.prop.indexOf("transition-")!==0)return;let a=AO.comma(s.value);if(s.prop==="transition-property"){a.forEach(o=>{let l=this.prefixes.add[o];l&&l.prefixes&&l.prefixes.length>0&&(i=!0)});return}return n=n||a.length>1,!1}),i&&n&&t.warn(e,"Replace transition-property to transition, because Autoprefixer could not support any cases of transition-property and other transition-*")}remove(e){let t=this.parse(e.value);t=t.filter(a=>{let o=this.prefixes.remove[this.findProp(a)];return!o||!o.remove});let i=this.stringify(t);if(e.value===i)return;if(t.length===0){e.remove();return}let n=e.parent.some(a=>a.prop===e.prop&&a.value===i),s=e.parent.some(a=>a!==e&&a.prop===e.prop&&a.value.length>i.length);if(n||s){e.remove();return}e.value=i}parse(e){let t=rb(e),i=[],n=[];for(let s of t.nodes)n.push(s),s.type==="div"&&s.value===","&&(i.push(n),n=[]);return i.push(n),i.filter(s=>s.length>0)}stringify(e){if(e.length===0)return"";let t=[];for(let i of e)i[i.length-1].type!=="div"&&i.push(this.div(e)),t=t.concat(i);return t[0].type==="div"&&(t=t.slice(1)),t[t.length-1].type==="div"&&(t=t.slice(0,-2+1||void 0)),rb.stringify({nodes:t})}clone(e,t,i){let n=[],s=!1;for(let a of i)!s&&a.type==="word"&&a.value===e?(n.push({type:"word",value:t}),s=!0):n.push(a);return n}div(e){for(let t of e)for(let i of t)if(i.type==="div"&&i.value===",")return i;return{type:"div",value:",",after:" "}}cleanOtherPrefixes(e,t){return e.filter(i=>{let n=ib.prefix(this.findProp(i));return n===""||n===t})}cleanFromUnprefixed(e,t){let i=e.map(s=>this.findProp(s)).filter(s=>s.slice(0,t.length)===t).map(s=>this.prefixes.unprefixed(s)),n=[];for(let s of e){let a=this.findProp(s),o=ib.prefix(a);!i.includes(a)&&(o===t||o==="")&&n.push(s)}return n}disabled(e,t){let i=["order","justify-content","align-self","align-content"];if(e.includes("flex")||i.includes(e)){if(this.prefixes.options.flexbox===!1)return!0;if(this.prefixes.options.flexbox==="no-2009")return t.includes("2009")}}ruleVendorPrefixes(e){let{parent:t}=e;if(t.type!=="rule")return!1;if(!t.selector.includes(":-"))return!1;let i=CO.prefixes().filter(n=>t.selector.includes(":"+n));return i.length>0?i:!1}};sb.exports=nb});var xr=x((f$,lb)=>{u();var _O=_e(),ob=class{constructor(e,t,i,n){this.unprefixed=e,this.prefixed=t,this.string=i||t,this.regexp=n||_O.regexp(t)}check(e){return e.includes(this.string)?!!e.match(this.regexp):!1}};lb.exports=ob});var He=x((c$,fb)=>{u();var EO=wr(),OO=xr(),TO=Ni(),RO=_e(),ub=class extends EO{static save(e,t){let i=t.prop,n=[];for(let s in t._autoprefixerValues){let a=t._autoprefixerValues[s];if(a===t.value)continue;let o,l=TO.prefix(i);if(l==="-pie-")continue;if(l===s){o=t.value=a,n.push(o);continue}let c=e.prefixed(i,s),f=t.parent;if(!f.every(b=>b.prop!==c)){n.push(o);continue}let d=a.replace(/\s+/," ");if(f.some(b=>b.prop===t.prop&&b.value.replace(/\s+/," ")===d)){n.push(o);continue}let h=this.clone(t,{value:a});o=t.parent.insertBefore(t,h),n.push(o)}return n}check(e){let t=e.value;return t.includes(this.name)?!!t.match(this.regexp()):!1}regexp(){return this.regexpCache||(this.regexpCache=RO.regexp(this.name))}replace(e,t){return e.replace(this.regexp(),`$1${t}$2`)}value(e){return e.raws.value&&e.raws.value.value===e.value?e.raws.value.raw:e.value}add(e,t){e._autoprefixerValues||(e._autoprefixerValues={});let i=e._autoprefixerValues[t]||this.value(e),n;do if(n=i,i=this.replace(i,t),i===!1)return;while(i!==n);e._autoprefixerValues[t]=i}old(e){return new OO(this.name,e+this.name)}};fb.exports=ub});var Bt=x((p$,cb)=>{u();cb.exports={}});var ou=x((d$,hb)=>{u();var pb=Ms(),PO=He(),IO=Bt().insertAreas,DO=/(^|[^-])linear-gradient\(\s*(top|left|right|bottom)/i,qO=/(^|[^-])radial-gradient\(\s*\d+(\w*|%)\s+\d+(\w*|%)\s*,/i,$O=/(!\s*)?autoprefixer:\s*ignore\s+next/i,LO=/(!\s*)?autoprefixer\s*grid:\s*(on|off|(no-)?autoplace)/i,MO=["width","height","min-width","max-width","min-height","max-height","inline-size","min-inline-size","max-inline-size","block-size","min-block-size","max-block-size"];function au(r){return r.parent.some(e=>e.prop==="grid-template"||e.prop==="grid-template-areas")}function NO(r){let e=r.parent.some(i=>i.prop==="grid-template-rows"),t=r.parent.some(i=>i.prop==="grid-template-columns");return e&&t}var db=class{constructor(e){this.prefixes=e}add(e,t){let i=this.prefixes.add["@resolution"],n=this.prefixes.add["@keyframes"],s=this.prefixes.add["@viewport"],a=this.prefixes.add["@supports"];e.walkAtRules(f=>{if(f.name==="keyframes"){if(!this.disabled(f,t))return n&&n.process(f)}else if(f.name==="viewport"){if(!this.disabled(f,t))return s&&s.process(f)}else if(f.name==="supports"){if(this.prefixes.options.supports!==!1&&!this.disabled(f,t))return a.process(f)}else if(f.name==="media"&&f.params.includes("-resolution")&&!this.disabled(f,t))return i&&i.process(f)}),e.walkRules(f=>{if(!this.disabled(f,t))return this.prefixes.add.selectors.map(d=>d.process(f,t))});function o(f){return f.parent.nodes.some(d=>{if(d.type!=="decl")return!1;let p=d.prop==="display"&&/(inline-)?grid/.test(d.value),h=d.prop.startsWith("grid-template"),b=/^grid-([A-z]+-)?gap/.test(d.prop);return p||h||b})}function l(f){return f.parent.some(d=>d.prop==="display"&&/(inline-)?flex/.test(d.value))}let c=this.gridStatus(e,t)&&this.prefixes.add["grid-area"]&&this.prefixes.add["grid-area"].prefixes;return e.walkDecls(f=>{if(this.disabledDecl(f,t))return;let d=f.parent,p=f.prop,h=f.value;if(p==="grid-row-span"){t.warn("grid-row-span is not part of final Grid Layout. Use grid-row.",{node:f});return}else if(p==="grid-column-span"){t.warn("grid-column-span is not part of final Grid Layout. Use grid-column.",{node:f});return}else if(p==="display"&&h==="box"){t.warn("You should write display: flex by final spec instead of display: box",{node:f});return}else if(p==="text-emphasis-position")(h==="under"||h==="over")&&t.warn("You should use 2 values for text-emphasis-position For example, `under left` instead of just `under`.",{node:f});else if(/^(align|justify|place)-(items|content)$/.test(p)&&l(f))(h==="start"||h==="end")&&t.warn(`${h} value has mixed support, consider using flex-${h} instead`,{node:f});else if(p==="text-decoration-skip"&&h==="ink")t.warn("Replace text-decoration-skip: ink to text-decoration-skip-ink: auto, because spec had been changed",{node:f});else{if(c&&this.gridStatus(f,t))if(f.value==="subgrid"&&t.warn("IE does not support subgrid",{node:f}),/^(align|justify|place)-items$/.test(p)&&o(f)){let v=p.replace("-items","-self");t.warn(`IE does not support ${p} on grid containers. Try using ${v} on child elements instead: ${f.parent.selector} > * { ${v}: ${f.value} }`,{node:f})}else if(/^(align|justify|place)-content$/.test(p)&&o(f))t.warn(`IE does not support ${f.prop} on grid containers`,{node:f});else if(p==="display"&&f.value==="contents"){t.warn("Please do not use display: contents; if you have grid setting enabled",{node:f});return}else if(f.prop==="grid-gap"){let v=this.gridStatus(f,t);v==="autoplace"&&!NO(f)&&!au(f)?t.warn("grid-gap only works if grid-template(-areas) is being used or both rows and columns have been declared and cells have not been manually placed inside the explicit grid",{node:f}):(v===!0||v==="no-autoplace")&&!au(f)&&t.warn("grid-gap only works if grid-template(-areas) is being used",{node:f})}else if(p==="grid-auto-columns"){t.warn("grid-auto-columns is not supported by IE",{node:f});return}else if(p==="grid-auto-rows"){t.warn("grid-auto-rows is not supported by IE",{node:f});return}else if(p==="grid-auto-flow"){let v=d.some(w=>w.prop==="grid-template-rows"),y=d.some(w=>w.prop==="grid-template-columns");au(f)?t.warn("grid-auto-flow is not supported by IE",{node:f}):h.includes("dense")?t.warn("grid-auto-flow: dense is not supported by IE",{node:f}):!v&&!y&&t.warn("grid-auto-flow works only if grid-template-rows and grid-template-columns are present in the same rule",{node:f});return}else if(h.includes("auto-fit")){t.warn("auto-fit value is not supported by IE",{node:f,word:"auto-fit"});return}else if(h.includes("auto-fill")){t.warn("auto-fill value is not supported by IE",{node:f,word:"auto-fill"});return}else p.startsWith("grid-template")&&h.includes("[")&&t.warn("Autoprefixer currently does not support line names. Try using grid-template-areas instead.",{node:f,word:"["});if(h.includes("radial-gradient"))if(qO.test(f.value))t.warn("Gradient has outdated direction syntax. New syntax is like `closest-side at 0 0` instead of `0 0, closest-side`.",{node:f});else{let v=pb(h);for(let y of v.nodes)if(y.type==="function"&&y.value==="radial-gradient")for(let w of y.nodes)w.type==="word"&&(w.value==="cover"?t.warn("Gradient has outdated direction syntax. Replace `cover` to `farthest-corner`.",{node:f}):w.value==="contain"&&t.warn("Gradient has outdated direction syntax. Replace `contain` to `closest-side`.",{node:f}))}h.includes("linear-gradient")&&DO.test(h)&&t.warn("Gradient has outdated direction syntax. New syntax is like `to left` instead of `right`.",{node:f})}MO.includes(f.prop)&&(f.value.includes("-fill-available")||(f.value.includes("fill-available")?t.warn("Replace fill-available to stretch, because spec had been changed",{node:f}):f.value.includes("fill")&&pb(h).nodes.some(y=>y.type==="word"&&y.value==="fill")&&t.warn("Replace fill to stretch, because spec had been changed",{node:f})));let b;if(f.prop==="transition"||f.prop==="transition-property")return this.prefixes.transition.add(f,t);if(f.prop==="align-self"){if(this.displayType(f)!=="grid"&&this.prefixes.options.flexbox!==!1&&(b=this.prefixes.add["align-self"],b&&b.prefixes&&b.process(f)),this.gridStatus(f,t)!==!1&&(b=this.prefixes.add["grid-row-align"],b&&b.prefixes))return b.process(f,t)}else if(f.prop==="justify-self"){if(this.gridStatus(f,t)!==!1&&(b=this.prefixes.add["grid-column-align"],b&&b.prefixes))return b.process(f,t)}else if(f.prop==="place-self"){if(b=this.prefixes.add["place-self"],b&&b.prefixes&&this.gridStatus(f,t)!==!1)return b.process(f,t)}else if(b=this.prefixes.add[f.prop],b&&b.prefixes)return b.process(f,t)}),this.gridStatus(e,t)&&IO(e,this.disabled),e.walkDecls(f=>{if(this.disabledValue(f,t))return;let d=this.prefixes.unprefixed(f.prop),p=this.prefixes.values("add",d);if(Array.isArray(p))for(let h of p)h.process&&h.process(f,t);PO.save(this.prefixes,f)})}remove(e,t){let i=this.prefixes.remove["@resolution"];e.walkAtRules((n,s)=>{this.prefixes.remove[`@${n.name}`]?this.disabled(n,t)||n.parent.removeChild(s):n.name==="media"&&n.params.includes("-resolution")&&i&&i.clean(n)});for(let n of this.prefixes.remove.selectors)e.walkRules((s,a)=>{n.check(s)&&(this.disabled(s,t)||s.parent.removeChild(a))});return e.walkDecls((n,s)=>{if(this.disabled(n,t))return;let a=n.parent,o=this.prefixes.unprefixed(n.prop);if((n.prop==="transition"||n.prop==="transition-property")&&this.prefixes.transition.remove(n),this.prefixes.remove[n.prop]&&this.prefixes.remove[n.prop].remove){let l=this.prefixes.group(n).down(c=>this.prefixes.normalize(c.prop)===o);if(o==="flex-flow"&&(l=!0),n.prop==="-webkit-box-orient"){let c={"flex-direction":!0,"flex-flow":!0};if(!n.parent.some(f=>c[f.prop]))return}if(l&&!this.withHackValue(n)){n.raw("before").includes(` +`)&&this.reduceSpaces(n),a.removeChild(s);return}}for(let l of this.prefixes.values("remove",o)){if(!l.check||!l.check(n.value))continue;if(o=l.unprefixed,this.prefixes.group(n).down(f=>f.value.includes(o))){a.removeChild(s);return}}})}withHackValue(e){return e.prop==="-webkit-background-clip"&&e.value==="text"}disabledValue(e,t){return this.gridStatus(e,t)===!1&&e.type==="decl"&&e.prop==="display"&&e.value.includes("grid")||this.prefixes.options.flexbox===!1&&e.type==="decl"&&e.prop==="display"&&e.value.includes("flex")||e.type==="decl"&&e.prop==="content"?!0:this.disabled(e,t)}disabledDecl(e,t){if(this.gridStatus(e,t)===!1&&e.type==="decl"&&(e.prop.includes("grid")||e.prop==="justify-items"))return!0;if(this.prefixes.options.flexbox===!1&&e.type==="decl"){let i=["order","justify-content","align-items","align-content"];if(e.prop.includes("flex")||i.includes(e.prop))return!0}return this.disabled(e,t)}disabled(e,t){if(!e)return!1;if(e._autoprefixerDisabled!==void 0)return e._autoprefixerDisabled;if(e.parent){let n=e.prev();if(n&&n.type==="comment"&&$O.test(n.text))return e._autoprefixerDisabled=!0,e._autoprefixerSelfDisabled=!0,!0}let i=null;if(e.nodes){let n;e.each(s=>{s.type==="comment"&&/(!\s*)?autoprefixer:\s*(off|on)/i.test(s.text)&&(typeof n!="undefined"?t.warn("Second Autoprefixer control comment was ignored. Autoprefixer applies control comment to whole block, not to next rules.",{node:s}):n=/on/i.test(s.text))}),n!==void 0&&(i=!n)}if(!e.nodes||i===null)if(e.parent){let n=this.disabled(e.parent,t);e.parent._autoprefixerSelfDisabled===!0?i=!1:i=n}else i=!1;return e._autoprefixerDisabled=i,i}reduceSpaces(e){let t=!1;if(this.prefixes.group(e).up(()=>(t=!0,!0)),t)return;let i=e.raw("before").split(` +`),n=i[i.length-1].length,s=!1;this.prefixes.group(e).down(a=>{i=a.raw("before").split(` +`);let o=i.length-1;i[o].length>n&&(s===!1&&(s=i[o].length-n),i[o]=i[o].slice(0,-s),a.raws.before=i.join(` +`))})}displayType(e){for(let t of e.parent.nodes)if(t.prop==="display"){if(t.value.includes("flex"))return"flex";if(t.value.includes("grid"))return"grid"}return!1}gridStatus(e,t){if(!e)return!1;if(e._autoprefixerGridStatus!==void 0)return e._autoprefixerGridStatus;let i=null;if(e.nodes){let n;e.each(s=>{if(s.type==="comment"&&LO.test(s.text)){let a=/:\s*autoplace/i.test(s.text),o=/no-autoplace/i.test(s.text);typeof n!="undefined"?t.warn("Second Autoprefixer grid control comment was ignored. Autoprefixer applies control comments to the whole block, not to the next rules.",{node:s}):a?n="autoplace":o?n=!0:n=/on/i.test(s.text)}}),n!==void 0&&(i=n)}if(e.type==="atrule"&&e.name==="supports"){let n=e.params;n.includes("grid")&&n.includes("auto")&&(i=!1)}if(!e.nodes||i===null)if(e.parent){let n=this.gridStatus(e.parent,t);e.parent._autoprefixerSelfDisabled===!0?i=!1:i=n}else typeof this.prefixes.options.grid!="undefined"?i=this.prefixes.options.grid:typeof m.env.AUTOPREFIXER_GRID!="undefined"?m.env.AUTOPREFIXER_GRID==="autoplace"?i="autoplace":i=!0:i=!1;return e._autoprefixerGridStatus=i,i}};hb.exports=db});var gb=x((h$,mb)=>{u();mb.exports={A:{A:{"2":"K E F G A B JC"},B:{"1":"C L M H N D O P Q R S T U V W X Y Z a b c d e f g h i j n o p q r s t u v w x y z I"},C:{"1":"2 3 4 5 6 7 8 9 AB BB CB DB EB FB GB HB IB JB KB LB MB NB OB PB QB RB SB TB UB VB WB XB YB ZB aB bB cB 0B dB 1B eB fB gB hB iB jB kB lB mB nB oB m pB qB rB sB tB P Q R 2B S T U V W X Y Z a b c d e f g h i j n o p q r s t u v w x y z I uB 3B 4B","2":"0 1 KC zB J K E F G A B C L M H N D O k l LC MC"},D:{"1":"8 9 AB BB CB DB EB FB GB HB IB JB KB LB MB NB OB PB QB RB SB TB UB VB WB XB YB ZB aB bB cB 0B dB 1B eB fB gB hB iB jB kB lB mB nB oB m pB qB rB sB tB P Q R S T U V W X Y Z a b c d e f g h i j n o p q r s t u v w x y z I uB 3B 4B","2":"0 1 2 3 4 5 6 7 J K E F G A B C L M H N D O k l"},E:{"1":"G A B C L M H D RC 6B vB wB 7B SC TC 8B 9B xB AC yB BC CC DC EC FC GC UC","2":"0 J K E F NC 5B OC PC QC"},F:{"1":"1 2 3 4 5 6 7 8 9 H N D O k l AB BB CB DB EB FB GB HB IB JB KB LB MB NB OB PB QB RB SB TB UB VB WB XB YB ZB aB bB cB dB eB fB gB hB iB jB kB lB mB nB oB m pB qB rB sB tB P Q R 2B S T U V W X Y Z a b c d e f g h i j wB","2":"G B C VC WC XC YC vB HC ZC"},G:{"1":"D fC gC hC iC jC kC lC mC nC oC pC qC rC sC tC 8B 9B xB AC yB BC CC DC EC FC GC","2":"F 5B aC IC bC cC dC eC"},H:{"1":"uC"},I:{"1":"I zC 0C","2":"zB J vC wC xC yC IC"},J:{"2":"E A"},K:{"1":"m","2":"A B C vB HC wB"},L:{"1":"I"},M:{"1":"uB"},N:{"2":"A B"},O:{"1":"xB"},P:{"1":"J k l 1C 2C 3C 4C 5C 6B 6C 7C 8C 9C AD yB BD CD DD"},Q:{"1":"7B"},R:{"1":"ED"},S:{"1":"FD GD"}},B:4,C:"CSS Feature Queries"}});var vb=x((m$,wb)=>{u();function yb(r){return r[r.length-1]}var bb={parse(r){let e=[""],t=[e];for(let i of r){if(i==="("){e=[""],yb(t).push(e),t.push(e);continue}if(i===")"){t.pop(),e=yb(t),e.push("");continue}e[e.length-1]+=i}return t[0]},stringify(r){let e="";for(let t of r){if(typeof t=="object"){e+=`(${bb.stringify(t)})`;continue}e+=t}return e}};wb.exports=bb});var Cb=x((g$,Ab)=>{u();var BO=gb(),{feature:FO}=(Ps(),Rs),{parse:jO}=$e(),zO=Mt(),lu=vb(),UO=He(),VO=_e(),xb=FO(BO),kb=[];for(let r in xb.stats){let e=xb.stats[r];for(let t in e){let i=e[t];/y/.test(i)&&kb.push(r+" "+t)}}var Sb=class{constructor(e,t){this.Prefixes=e,this.all=t}prefixer(){if(this.prefixerCache)return this.prefixerCache;let e=this.all.browsers.selected.filter(i=>kb.includes(i)),t=new zO(this.all.browsers.data,e,this.all.options);return this.prefixerCache=new this.Prefixes(this.all.data,t,this.all.options),this.prefixerCache}parse(e){let t=e.split(":"),i=t[0],n=t[1];return n||(n=""),[i.trim(),n.trim()]}virtual(e){let[t,i]=this.parse(e),n=jO("a{}").first;return n.append({prop:t,value:i,raws:{before:""}}),n}prefixed(e){let t=this.virtual(e);if(this.disabled(t.first))return t.nodes;let i={warn:()=>null},n=this.prefixer().add[t.first.prop];n&&n.process&&n.process(t.first,i);for(let s of t.nodes){for(let a of this.prefixer().values("add",t.first.prop))a.process(s);UO.save(this.all,s)}return t.nodes}isNot(e){return typeof e=="string"&&/not\s*/i.test(e)}isOr(e){return typeof e=="string"&&/\s*or\s*/i.test(e)}isProp(e){return typeof e=="object"&&e.length===1&&typeof e[0]=="string"}isHack(e,t){return!new RegExp(`(\\(|\\s)${VO.escapeRegexp(t)}:`).test(e)}toRemove(e,t){let[i,n]=this.parse(e),s=this.all.unprefixed(i),a=this.all.cleaner();if(a.remove[i]&&a.remove[i].remove&&!this.isHack(t,s))return!0;for(let o of a.values("remove",s))if(o.check(n))return!0;return!1}remove(e,t){let i=0;for(;itypeof t!="object"?t:t.length===1&&typeof t[0]=="object"?this.cleanBrackets(t[0]):this.cleanBrackets(t))}convert(e){let t=[""];for(let i of e)t.push([`${i.prop}: ${i.value}`]),t.push(" or ");return t[t.length-1]="",t}normalize(e){if(typeof e!="object")return e;if(e=e.filter(t=>t!==""),typeof e[0]=="string"){let t=e[0].trim();if(t.includes(":")||t==="selector"||t==="not selector")return[lu.stringify(e)]}return e.map(t=>this.normalize(t))}add(e,t){return e.map(i=>{if(this.isProp(i)){let n=this.prefixed(i[0]);return n.length>1?this.convert(n):i}return typeof i=="object"?this.add(i,t):i})}process(e){let t=lu.parse(e.params);t=this.normalize(t),t=this.remove(t,e.params),t=this.add(t,e.params),t=this.cleanBrackets(t),e.params=lu.stringify(t)}disabled(e){if(!this.all.options.grid&&(e.prop==="display"&&e.value.includes("grid")||e.prop.includes("grid")||e.prop==="justify-items"))return!0;if(this.all.options.flexbox===!1){if(e.prop==="display"&&e.value.includes("flex"))return!0;let t=["order","justify-content","align-items","align-content"];if(e.prop.includes("flex")||t.includes(e.prop))return!0}return!1}};Ab.exports=Sb});var Ob=x((y$,Eb)=>{u();var _b=class{constructor(e,t){this.prefix=t,this.prefixed=e.prefixed(this.prefix),this.regexp=e.regexp(this.prefix),this.prefixeds=e.possible().map(i=>[e.prefixed(i),e.regexp(i)]),this.unprefixed=e.name,this.nameRegexp=e.regexp()}isHack(e){let t=e.parent.index(e)+1,i=e.parent.nodes;for(;t{u();var{list:HO}=$e(),WO=Ob(),GO=wr(),QO=Mt(),YO=_e(),Tb=class extends GO{constructor(e,t,i){super(e,t,i);this.regexpCache=new Map}check(e){return e.selector.includes(this.name)?!!e.selector.match(this.regexp()):!1}prefixed(e){return this.name.replace(/^(\W*)/,`$1${e}`)}regexp(e){if(!this.regexpCache.has(e)){let t=e?this.prefixed(e):this.name;this.regexpCache.set(e,new RegExp(`(^|[^:"'=])${YO.escapeRegexp(t)}`,"gi"))}return this.regexpCache.get(e)}possible(){return QO.prefixes()}prefixeds(e){if(e._autoprefixerPrefixeds){if(e._autoprefixerPrefixeds[this.name])return e._autoprefixerPrefixeds}else e._autoprefixerPrefixeds={};let t={};if(e.selector.includes(",")){let n=HO.comma(e.selector).filter(s=>s.includes(this.name));for(let s of this.possible())t[s]=n.map(a=>this.replace(a,s)).join(", ")}else for(let i of this.possible())t[i]=this.replace(e.selector,i);return e._autoprefixerPrefixeds[this.name]=t,e._autoprefixerPrefixeds}already(e,t,i){let n=e.parent.index(e)-1;for(;n>=0;){let s=e.parent.nodes[n];if(s.type!=="rule")return!1;let a=!1;for(let o in t[this.name]){let l=t[this.name][o];if(s.selector===l){if(i===o)return!0;a=!0;break}}if(!a)return!1;n-=1}return!1}replace(e,t){return e.replace(this.regexp(),`$1${this.prefixed(t)}`)}add(e,t){let i=this.prefixeds(e);if(this.already(e,i,t))return;let n=this.clone(e,{selector:i[this.name][t]});e.parent.insertBefore(e,n)}old(e){return new WO(this,e)}};Rb.exports=Tb});var Db=x((w$,Ib)=>{u();var KO=wr(),Pb=class extends KO{add(e,t){let i=t+e.name;if(e.parent.some(a=>a.name===i&&a.params===e.params))return;let s=this.clone(e,{name:i});return e.parent.insertBefore(e,s)}process(e){let t=this.parentPrefix(e);for(let i of this.prefixes)(!t||t===i)&&this.add(e,i)}};Ib.exports=Pb});var $b=x((v$,qb)=>{u();var XO=kr(),uu=class extends XO{prefixed(e){return e==="-webkit-"?":-webkit-full-screen":e==="-moz-"?":-moz-full-screen":`:${e}fullscreen`}};uu.names=[":fullscreen"];qb.exports=uu});var Mb=x((x$,Lb)=>{u();var ZO=kr(),fu=class extends ZO{possible(){return super.possible().concat(["-moz- old","-ms- old"])}prefixed(e){return e==="-webkit-"?"::-webkit-input-placeholder":e==="-ms-"?"::-ms-input-placeholder":e==="-ms- old"?":-ms-input-placeholder":e==="-moz- old"?":-moz-placeholder":`::${e}placeholder`}};fu.names=["::placeholder"];Lb.exports=fu});var Bb=x((k$,Nb)=>{u();var JO=kr(),cu=class extends JO{prefixed(e){return e==="-ms-"?":-ms-input-placeholder":`:${e}placeholder-shown`}};cu.names=[":placeholder-shown"];Nb.exports=cu});var jb=x((S$,Fb)=>{u();var eT=kr(),tT=_e(),pu=class extends eT{constructor(e,t,i){super(e,t,i);this.prefixes&&(this.prefixes=tT.uniq(this.prefixes.map(n=>"-webkit-")))}prefixed(e){return e==="-webkit-"?"::-webkit-file-upload-button":`::${e}file-selector-button`}};pu.names=["::file-selector-button"];Fb.exports=pu});var Pe=x((A$,zb)=>{u();zb.exports=function(r){let e;return r==="-webkit- 2009"||r==="-moz-"?e=2009:r==="-ms-"?e=2012:r==="-webkit-"&&(e="final"),r==="-webkit- 2009"&&(r="-webkit-"),[e,r]}});var Wb=x((C$,Hb)=>{u();var Ub=$e().list,Vb=Pe(),rT=j(),Sr=class extends rT{prefixed(e,t){let i;return[i,t]=Vb(t),i===2009?t+"box-flex":super.prefixed(e,t)}normalize(){return"flex"}set(e,t){let i=Vb(t)[0];if(i===2009)return e.value=Ub.space(e.value)[0],e.value=Sr.oldValues[e.value]||e.value,super.set(e,t);if(i===2012){let n=Ub.space(e.value);n.length===3&&n[2]==="0"&&(e.value=n.slice(0,2).concat("0px").join(" "))}return super.set(e,t)}};Sr.names=["flex","box-flex"];Sr.oldValues={auto:"1",none:"0"};Hb.exports=Sr});var Yb=x((_$,Qb)=>{u();var Gb=Pe(),iT=j(),du=class extends iT{prefixed(e,t){let i;return[i,t]=Gb(t),i===2009?t+"box-ordinal-group":i===2012?t+"flex-order":super.prefixed(e,t)}normalize(){return"order"}set(e,t){return Gb(t)[0]===2009&&/\d/.test(e.value)?(e.value=(parseInt(e.value)+1).toString(),super.set(e,t)):super.set(e,t)}};du.names=["order","flex-order","box-ordinal-group"];Qb.exports=du});var Xb=x((E$,Kb)=>{u();var nT=j(),hu=class extends nT{check(e){let t=e.value;return!t.toLowerCase().includes("alpha(")&&!t.includes("DXImageTransform.Microsoft")&&!t.includes("data:image/svg+xml")}};hu.names=["filter"];Kb.exports=hu});var Jb=x((O$,Zb)=>{u();var sT=j(),mu=class extends sT{insert(e,t,i,n){if(t!=="-ms-")return super.insert(e,t,i);let s=this.clone(e),a=e.prop.replace(/end$/,"start"),o=t+e.prop.replace(/end$/,"span");if(!e.parent.some(l=>l.prop===o)){if(s.prop=o,e.value.includes("span"))s.value=e.value.replace(/span\s/i,"");else{let l;if(e.parent.walkDecls(a,c=>{l=c}),l){let c=Number(e.value)-Number(l.value)+"";s.value=c}else e.warn(n,`Can not prefix ${e.prop} (${a} is not found)`)}e.cloneBefore(s)}}};mu.names=["grid-row-end","grid-column-end"];Zb.exports=mu});var tw=x((T$,ew)=>{u();var aT=j(),gu=class extends aT{check(e){return!e.value.split(/\s+/).some(t=>{let i=t.toLowerCase();return i==="reverse"||i==="alternate-reverse"})}};gu.names=["animation","animation-direction"];ew.exports=gu});var iw=x((R$,rw)=>{u();var oT=Pe(),lT=j(),yu=class extends lT{insert(e,t,i){let n;if([n,t]=oT(t),n!==2009)return super.insert(e,t,i);let s=e.value.split(/\s+/).filter(d=>d!=="wrap"&&d!=="nowrap"&&"wrap-reverse");if(s.length===0||e.parent.some(d=>d.prop===t+"box-orient"||d.prop===t+"box-direction"))return;let o=s[0],l=o.includes("row")?"horizontal":"vertical",c=o.includes("reverse")?"reverse":"normal",f=this.clone(e);return f.prop=t+"box-orient",f.value=l,this.needCascade(e)&&(f.raws.before=this.calcBefore(i,e,t)),e.parent.insertBefore(e,f),f=this.clone(e),f.prop=t+"box-direction",f.value=c,this.needCascade(e)&&(f.raws.before=this.calcBefore(i,e,t)),e.parent.insertBefore(e,f)}};yu.names=["flex-flow","box-direction","box-orient"];rw.exports=yu});var sw=x((P$,nw)=>{u();var uT=Pe(),fT=j(),bu=class extends fT{normalize(){return"flex"}prefixed(e,t){let i;return[i,t]=uT(t),i===2009?t+"box-flex":i===2012?t+"flex-positive":super.prefixed(e,t)}};bu.names=["flex-grow","flex-positive"];nw.exports=bu});var ow=x((I$,aw)=>{u();var cT=Pe(),pT=j(),wu=class extends pT{set(e,t){if(cT(t)[0]!==2009)return super.set(e,t)}};wu.names=["flex-wrap"];aw.exports=wu});var uw=x((D$,lw)=>{u();var dT=j(),Ar=Bt(),vu=class extends dT{insert(e,t,i,n){if(t!=="-ms-")return super.insert(e,t,i);let s=Ar.parse(e),[a,o]=Ar.translate(s,0,2),[l,c]=Ar.translate(s,1,3);[["grid-row",a],["grid-row-span",o],["grid-column",l],["grid-column-span",c]].forEach(([f,d])=>{Ar.insertDecl(e,f,d)}),Ar.warnTemplateSelectorNotFound(e,n),Ar.warnIfGridRowColumnExists(e,n)}};vu.names=["grid-area"];lw.exports=vu});var cw=x((q$,fw)=>{u();var hT=j(),Bi=Bt(),xu=class extends hT{insert(e,t,i){if(t!=="-ms-")return super.insert(e,t,i);if(e.parent.some(a=>a.prop==="-ms-grid-row-align"))return;let[[n,s]]=Bi.parse(e);s?(Bi.insertDecl(e,"grid-row-align",n),Bi.insertDecl(e,"grid-column-align",s)):(Bi.insertDecl(e,"grid-row-align",n),Bi.insertDecl(e,"grid-column-align",n))}};xu.names=["place-self"];fw.exports=xu});var dw=x(($$,pw)=>{u();var mT=j(),ku=class extends mT{check(e){let t=e.value;return!t.includes("/")||t.includes("span")}normalize(e){return e.replace("-start","")}prefixed(e,t){let i=super.prefixed(e,t);return t==="-ms-"&&(i=i.replace("-start","")),i}};ku.names=["grid-row-start","grid-column-start"];pw.exports=ku});var gw=x((L$,mw)=>{u();var hw=Pe(),gT=j(),Cr=class extends gT{check(e){return e.parent&&!e.parent.some(t=>t.prop&&t.prop.startsWith("grid-"))}prefixed(e,t){let i;return[i,t]=hw(t),i===2012?t+"flex-item-align":super.prefixed(e,t)}normalize(){return"align-self"}set(e,t){let i=hw(t)[0];if(i===2012)return e.value=Cr.oldValues[e.value]||e.value,super.set(e,t);if(i==="final")return super.set(e,t)}};Cr.names=["align-self","flex-item-align"];Cr.oldValues={"flex-end":"end","flex-start":"start"};mw.exports=Cr});var bw=x((M$,yw)=>{u();var yT=j(),bT=_e(),Su=class extends yT{constructor(e,t,i){super(e,t,i);this.prefixes&&(this.prefixes=bT.uniq(this.prefixes.map(n=>n==="-ms-"?"-webkit-":n)))}};Su.names=["appearance"];yw.exports=Su});var xw=x((N$,vw)=>{u();var ww=Pe(),wT=j(),Au=class extends wT{normalize(){return"flex-basis"}prefixed(e,t){let i;return[i,t]=ww(t),i===2012?t+"flex-preferred-size":super.prefixed(e,t)}set(e,t){let i;if([i,t]=ww(t),i===2012||i==="final")return super.set(e,t)}};Au.names=["flex-basis","flex-preferred-size"];vw.exports=Au});var Sw=x((B$,kw)=>{u();var vT=j(),Cu=class extends vT{normalize(){return this.name.replace("box-image","border")}prefixed(e,t){let i=super.prefixed(e,t);return t==="-webkit-"&&(i=i.replace("border","box-image")),i}};Cu.names=["mask-border","mask-border-source","mask-border-slice","mask-border-width","mask-border-outset","mask-border-repeat","mask-box-image","mask-box-image-source","mask-box-image-slice","mask-box-image-width","mask-box-image-outset","mask-box-image-repeat"];kw.exports=Cu});var Cw=x((F$,Aw)=>{u();var xT=j(),lt=class extends xT{insert(e,t,i){let n=e.prop==="mask-composite",s;n?s=e.value.split(","):s=e.value.match(lt.regexp)||[],s=s.map(c=>c.trim()).filter(c=>c);let a=s.length,o;if(a&&(o=this.clone(e),o.value=s.map(c=>lt.oldValues[c]||c).join(", "),s.includes("intersect")&&(o.value+=", xor"),o.prop=t+"mask-composite"),n)return a?(this.needCascade(e)&&(o.raws.before=this.calcBefore(i,e,t)),e.parent.insertBefore(e,o)):void 0;let l=this.clone(e);return l.prop=t+l.prop,a&&(l.value=l.value.replace(lt.regexp,"")),this.needCascade(e)&&(l.raws.before=this.calcBefore(i,e,t)),e.parent.insertBefore(e,l),a?(this.needCascade(e)&&(o.raws.before=this.calcBefore(i,e,t)),e.parent.insertBefore(e,o)):e}};lt.names=["mask","mask-composite"];lt.oldValues={add:"source-over",subtract:"source-out",intersect:"source-in",exclude:"xor"};lt.regexp=new RegExp(`\\s+(${Object.keys(lt.oldValues).join("|")})\\b(?!\\))\\s*(?=[,])`,"ig");Aw.exports=lt});var Ow=x((j$,Ew)=>{u();var _w=Pe(),kT=j(),_r=class extends kT{prefixed(e,t){let i;return[i,t]=_w(t),i===2009?t+"box-align":i===2012?t+"flex-align":super.prefixed(e,t)}normalize(){return"align-items"}set(e,t){let i=_w(t)[0];return(i===2009||i===2012)&&(e.value=_r.oldValues[e.value]||e.value),super.set(e,t)}};_r.names=["align-items","flex-align","box-align"];_r.oldValues={"flex-end":"end","flex-start":"start"};Ew.exports=_r});var Rw=x((z$,Tw)=>{u();var ST=j(),_u=class extends ST{set(e,t){return t==="-ms-"&&e.value==="contain"&&(e.value="element"),super.set(e,t)}insert(e,t,i){if(!(e.value==="all"&&t==="-ms-"))return super.insert(e,t,i)}};_u.names=["user-select"];Tw.exports=_u});var Dw=x((U$,Iw)=>{u();var Pw=Pe(),AT=j(),Eu=class extends AT{normalize(){return"flex-shrink"}prefixed(e,t){let i;return[i,t]=Pw(t),i===2012?t+"flex-negative":super.prefixed(e,t)}set(e,t){let i;if([i,t]=Pw(t),i===2012||i==="final")return super.set(e,t)}};Eu.names=["flex-shrink","flex-negative"];Iw.exports=Eu});var $w=x((V$,qw)=>{u();var CT=j(),Ou=class extends CT{prefixed(e,t){return`${t}column-${e}`}normalize(e){return e.includes("inside")?"break-inside":e.includes("before")?"break-before":"break-after"}set(e,t){return(e.prop==="break-inside"&&e.value==="avoid-column"||e.value==="avoid-page")&&(e.value="avoid"),super.set(e,t)}insert(e,t,i){if(e.prop!=="break-inside")return super.insert(e,t,i);if(!(/region/i.test(e.value)||/page/i.test(e.value)))return super.insert(e,t,i)}};Ou.names=["break-inside","page-break-inside","column-break-inside","break-before","page-break-before","column-break-before","break-after","page-break-after","column-break-after"];qw.exports=Ou});var Mw=x((H$,Lw)=>{u();var _T=j(),Tu=class extends _T{prefixed(e,t){return t+"print-color-adjust"}normalize(){return"color-adjust"}};Tu.names=["color-adjust","print-color-adjust"];Lw.exports=Tu});var Bw=x((W$,Nw)=>{u();var ET=j(),Er=class extends ET{insert(e,t,i){if(t==="-ms-"){let n=this.set(this.clone(e),t);this.needCascade(e)&&(n.raws.before=this.calcBefore(i,e,t));let s="ltr";return e.parent.nodes.forEach(a=>{a.prop==="direction"&&(a.value==="rtl"||a.value==="ltr")&&(s=a.value)}),n.value=Er.msValues[s][e.value]||e.value,e.parent.insertBefore(e,n)}return super.insert(e,t,i)}};Er.names=["writing-mode"];Er.msValues={ltr:{"horizontal-tb":"lr-tb","vertical-rl":"tb-rl","vertical-lr":"tb-lr"},rtl:{"horizontal-tb":"rl-tb","vertical-rl":"bt-rl","vertical-lr":"bt-lr"}};Nw.exports=Er});var jw=x((G$,Fw)=>{u();var OT=j(),Ru=class extends OT{set(e,t){return e.value=e.value.replace(/\s+fill(\s)/,"$1"),super.set(e,t)}};Ru.names=["border-image"];Fw.exports=Ru});var Vw=x((Q$,Uw)=>{u();var zw=Pe(),TT=j(),Or=class extends TT{prefixed(e,t){let i;return[i,t]=zw(t),i===2012?t+"flex-line-pack":super.prefixed(e,t)}normalize(){return"align-content"}set(e,t){let i=zw(t)[0];if(i===2012)return e.value=Or.oldValues[e.value]||e.value,super.set(e,t);if(i==="final")return super.set(e,t)}};Or.names=["align-content","flex-line-pack"];Or.oldValues={"flex-end":"end","flex-start":"start","space-between":"justify","space-around":"distribute"};Uw.exports=Or});var Ww=x((Y$,Hw)=>{u();var RT=j(),We=class extends RT{prefixed(e,t){return t==="-moz-"?t+(We.toMozilla[e]||e):super.prefixed(e,t)}normalize(e){return We.toNormal[e]||e}};We.names=["border-radius"];We.toMozilla={};We.toNormal={};for(let r of["top","bottom"])for(let e of["left","right"]){let t=`border-${r}-${e}-radius`,i=`border-radius-${r}${e}`;We.names.push(t),We.names.push(i),We.toMozilla[t]=i,We.toNormal[i]=t}Hw.exports=We});var Qw=x((K$,Gw)=>{u();var PT=j(),Pu=class extends PT{prefixed(e,t){return e.includes("-start")?t+e.replace("-block-start","-before"):t+e.replace("-block-end","-after")}normalize(e){return e.includes("-before")?e.replace("-before","-block-start"):e.replace("-after","-block-end")}};Pu.names=["border-block-start","border-block-end","margin-block-start","margin-block-end","padding-block-start","padding-block-end","border-before","border-after","margin-before","margin-after","padding-before","padding-after"];Gw.exports=Pu});var Kw=x((X$,Yw)=>{u();var IT=j(),{parseTemplate:DT,warnMissedAreas:qT,getGridGap:$T,warnGridGap:LT,inheritGridGap:MT}=Bt(),Iu=class extends IT{insert(e,t,i,n){if(t!=="-ms-")return super.insert(e,t,i);if(e.parent.some(h=>h.prop==="-ms-grid-rows"))return;let s=$T(e),a=MT(e,s),{rows:o,columns:l,areas:c}=DT({decl:e,gap:a||s}),f=Object.keys(c).length>0,d=Boolean(o),p=Boolean(l);return LT({gap:s,hasColumns:p,decl:e,result:n}),qT(c,e,n),(d&&p||f)&&e.cloneBefore({prop:"-ms-grid-rows",value:o,raws:{}}),p&&e.cloneBefore({prop:"-ms-grid-columns",value:l,raws:{}}),e}};Iu.names=["grid-template"];Yw.exports=Iu});var Zw=x((Z$,Xw)=>{u();var NT=j(),Du=class extends NT{prefixed(e,t){return t+e.replace("-inline","")}normalize(e){return e.replace(/(margin|padding|border)-(start|end)/,"$1-inline-$2")}};Du.names=["border-inline-start","border-inline-end","margin-inline-start","margin-inline-end","padding-inline-start","padding-inline-end","border-start","border-end","margin-start","margin-end","padding-start","padding-end"];Xw.exports=Du});var e0=x((J$,Jw)=>{u();var BT=j(),qu=class extends BT{check(e){return!e.value.includes("flex-")&&e.value!=="baseline"}prefixed(e,t){return t+"grid-row-align"}normalize(){return"align-self"}};qu.names=["grid-row-align"];Jw.exports=qu});var r0=x((eL,t0)=>{u();var FT=j(),Tr=class extends FT{keyframeParents(e){let{parent:t}=e;for(;t;){if(t.type==="atrule"&&t.name==="keyframes")return!0;({parent:t}=t)}return!1}contain3d(e){if(e.prop==="transform-origin")return!1;for(let t of Tr.functions3d)if(e.value.includes(`${t}(`))return!0;return!1}set(e,t){return e=super.set(e,t),t==="-ms-"&&(e.value=e.value.replace(/rotatez/gi,"rotate")),e}insert(e,t,i){if(t==="-ms-"){if(!this.contain3d(e)&&!this.keyframeParents(e))return super.insert(e,t,i)}else if(t==="-o-"){if(!this.contain3d(e))return super.insert(e,t,i)}else return super.insert(e,t,i)}};Tr.names=["transform","transform-origin"];Tr.functions3d=["matrix3d","translate3d","translateZ","scale3d","scaleZ","rotate3d","rotateX","rotateY","perspective"];t0.exports=Tr});var s0=x((tL,n0)=>{u();var i0=Pe(),jT=j(),$u=class extends jT{normalize(){return"flex-direction"}insert(e,t,i){let n;if([n,t]=i0(t),n!==2009)return super.insert(e,t,i);if(e.parent.some(f=>f.prop===t+"box-orient"||f.prop===t+"box-direction"))return;let a=e.value,o,l;a==="inherit"||a==="initial"||a==="unset"?(o=a,l=a):(o=a.includes("row")?"horizontal":"vertical",l=a.includes("reverse")?"reverse":"normal");let c=this.clone(e);return c.prop=t+"box-orient",c.value=o,this.needCascade(e)&&(c.raws.before=this.calcBefore(i,e,t)),e.parent.insertBefore(e,c),c=this.clone(e),c.prop=t+"box-direction",c.value=l,this.needCascade(e)&&(c.raws.before=this.calcBefore(i,e,t)),e.parent.insertBefore(e,c)}old(e,t){let i;return[i,t]=i0(t),i===2009?[t+"box-orient",t+"box-direction"]:super.old(e,t)}};$u.names=["flex-direction","box-direction","box-orient"];n0.exports=$u});var o0=x((rL,a0)=>{u();var zT=j(),Lu=class extends zT{check(e){return e.value==="pixelated"}prefixed(e,t){return t==="-ms-"?"-ms-interpolation-mode":super.prefixed(e,t)}set(e,t){return t!=="-ms-"?super.set(e,t):(e.prop="-ms-interpolation-mode",e.value="nearest-neighbor",e)}normalize(){return"image-rendering"}process(e,t){return super.process(e,t)}};Lu.names=["image-rendering","interpolation-mode"];a0.exports=Lu});var u0=x((iL,l0)=>{u();var UT=j(),VT=_e(),Mu=class extends UT{constructor(e,t,i){super(e,t,i);this.prefixes&&(this.prefixes=VT.uniq(this.prefixes.map(n=>n==="-ms-"?"-webkit-":n)))}};Mu.names=["backdrop-filter"];l0.exports=Mu});var c0=x((nL,f0)=>{u();var HT=j(),WT=_e(),Nu=class extends HT{constructor(e,t,i){super(e,t,i);this.prefixes&&(this.prefixes=WT.uniq(this.prefixes.map(n=>n==="-ms-"?"-webkit-":n)))}check(e){return e.value.toLowerCase()==="text"}};Nu.names=["background-clip"];f0.exports=Nu});var d0=x((sL,p0)=>{u();var GT=j(),QT=["none","underline","overline","line-through","blink","inherit","initial","unset"],Bu=class extends GT{check(e){return e.value.split(/\s+/).some(t=>!QT.includes(t))}};Bu.names=["text-decoration"];p0.exports=Bu});var g0=x((aL,m0)=>{u();var h0=Pe(),YT=j(),Rr=class extends YT{prefixed(e,t){let i;return[i,t]=h0(t),i===2009?t+"box-pack":i===2012?t+"flex-pack":super.prefixed(e,t)}normalize(){return"justify-content"}set(e,t){let i=h0(t)[0];if(i===2009||i===2012){let n=Rr.oldValues[e.value]||e.value;if(e.value=n,i!==2009||n!=="distribute")return super.set(e,t)}else if(i==="final")return super.set(e,t)}};Rr.names=["justify-content","flex-pack","box-pack"];Rr.oldValues={"flex-end":"end","flex-start":"start","space-between":"justify","space-around":"distribute"};m0.exports=Rr});var b0=x((oL,y0)=>{u();var KT=j(),Fu=class extends KT{set(e,t){let i=e.value.toLowerCase();return t==="-webkit-"&&!i.includes(" ")&&i!=="contain"&&i!=="cover"&&(e.value=e.value+" "+e.value),super.set(e,t)}};Fu.names=["background-size"];y0.exports=Fu});var v0=x((lL,w0)=>{u();var XT=j(),ju=Bt(),zu=class extends XT{insert(e,t,i){if(t!=="-ms-")return super.insert(e,t,i);let n=ju.parse(e),[s,a]=ju.translate(n,0,1);n[0]&&n[0].includes("span")&&(a=n[0].join("").replace(/\D/g,"")),[[e.prop,s],[`${e.prop}-span`,a]].forEach(([l,c])=>{ju.insertDecl(e,l,c)})}};zu.names=["grid-row","grid-column"];w0.exports=zu});var S0=x((uL,k0)=>{u();var ZT=j(),{prefixTrackProp:x0,prefixTrackValue:JT,autoplaceGridItems:eR,getGridGap:tR,inheritGridGap:rR}=Bt(),iR=ou(),Uu=class extends ZT{prefixed(e,t){return t==="-ms-"?x0({prop:e,prefix:t}):super.prefixed(e,t)}normalize(e){return e.replace(/^grid-(rows|columns)/,"grid-template-$1")}insert(e,t,i,n){if(t!=="-ms-")return super.insert(e,t,i);let{parent:s,prop:a,value:o}=e,l=a.includes("rows"),c=a.includes("columns"),f=s.some(k=>k.prop==="grid-template"||k.prop==="grid-template-areas");if(f&&l)return!1;let d=new iR({options:{}}),p=d.gridStatus(s,n),h=tR(e);h=rR(e,h)||h;let b=l?h.row:h.column;(p==="no-autoplace"||p===!0)&&!f&&(b=null);let v=JT({value:o,gap:b});e.cloneBefore({prop:x0({prop:a,prefix:t}),value:v});let y=s.nodes.find(k=>k.prop==="grid-auto-flow"),w="row";if(y&&!d.disabled(y,n)&&(w=y.value.trim()),p==="autoplace"){let k=s.nodes.find(E=>E.prop==="grid-template-rows");if(!k&&f)return;if(!k&&!f){e.warn(n,"Autoplacement does not work without grid-template-rows property");return}!s.nodes.find(E=>E.prop==="grid-template-columns")&&!f&&e.warn(n,"Autoplacement does not work without grid-template-columns property"),c&&!f&&eR(e,n,h,w)}}};Uu.names=["grid-template-rows","grid-template-columns","grid-rows","grid-columns"];k0.exports=Uu});var C0=x((fL,A0)=>{u();var nR=j(),Vu=class extends nR{check(e){return!e.value.includes("flex-")&&e.value!=="baseline"}prefixed(e,t){return t+"grid-column-align"}normalize(){return"justify-self"}};Vu.names=["grid-column-align"];A0.exports=Vu});var E0=x((cL,_0)=>{u();var sR=j(),Hu=class extends sR{prefixed(e,t){return t+"scroll-chaining"}normalize(){return"overscroll-behavior"}set(e,t){return e.value==="auto"?e.value="chained":(e.value==="none"||e.value==="contain")&&(e.value="none"),super.set(e,t)}};Hu.names=["overscroll-behavior","scroll-chaining"];_0.exports=Hu});var R0=x((pL,T0)=>{u();var aR=j(),{parseGridAreas:oR,warnMissedAreas:lR,prefixTrackProp:uR,prefixTrackValue:O0,getGridGap:fR,warnGridGap:cR,inheritGridGap:pR}=Bt();function dR(r){return r.trim().slice(1,-1).split(/["']\s*["']?/g)}var Wu=class extends aR{insert(e,t,i,n){if(t!=="-ms-")return super.insert(e,t,i);let s=!1,a=!1,o=e.parent,l=fR(e);l=pR(e,l)||l,o.walkDecls(/-ms-grid-rows/,d=>d.remove()),o.walkDecls(/grid-template-(rows|columns)/,d=>{if(d.prop==="grid-template-rows"){a=!0;let{prop:p,value:h}=d;d.cloneBefore({prop:uR({prop:p,prefix:t}),value:O0({value:h,gap:l.row})})}else s=!0});let c=dR(e.value);s&&!a&&l.row&&c.length>1&&e.cloneBefore({prop:"-ms-grid-rows",value:O0({value:`repeat(${c.length}, auto)`,gap:l.row}),raws:{}}),cR({gap:l,hasColumns:s,decl:e,result:n});let f=oR({rows:c,gap:l});return lR(f,e,n),e}};Wu.names=["grid-template-areas"];T0.exports=Wu});var I0=x((dL,P0)=>{u();var hR=j(),Gu=class extends hR{set(e,t){return t==="-webkit-"&&(e.value=e.value.replace(/\s*(right|left)\s*/i,"")),super.set(e,t)}};Gu.names=["text-emphasis-position"];P0.exports=Gu});var q0=x((hL,D0)=>{u();var mR=j(),Qu=class extends mR{set(e,t){return e.prop==="text-decoration-skip-ink"&&e.value==="auto"?(e.prop=t+"text-decoration-skip",e.value="ink",e):super.set(e,t)}};Qu.names=["text-decoration-skip-ink","text-decoration-skip"];D0.exports=Qu});var F0=x((mL,B0)=>{u();"use strict";B0.exports={wrap:$0,limit:L0,validate:M0,test:Yu,curry:gR,name:N0};function $0(r,e,t){var i=e-r;return((t-r)%i+i)%i+r}function L0(r,e,t){return Math.max(r,Math.min(e,t))}function M0(r,e,t,i,n){if(!Yu(r,e,t,i,n))throw new Error(t+" is outside of range ["+r+","+e+")");return t}function Yu(r,e,t,i,n){return!(te||n&&t===e||i&&t===r)}function N0(r,e,t,i){return(t?"(":"[")+r+","+e+(i?")":"]")}function gR(r,e,t,i){var n=N0.bind(null,r,e,t,i);return{wrap:$0.bind(null,r,e),limit:L0.bind(null,r,e),validate:function(s){return M0(r,e,s,t,i)},test:function(s){return Yu(r,e,s,t,i)},toString:n,name:n}}});var U0=x((gL,z0)=>{u();var Ku=Ms(),yR=F0(),bR=xr(),wR=He(),vR=_e(),j0=/top|left|right|bottom/gi,wt=class extends wR{replace(e,t){let i=Ku(e);for(let n of i.nodes)if(n.type==="function"&&n.value===this.name)if(n.nodes=this.newDirection(n.nodes),n.nodes=this.normalize(n.nodes),t==="-webkit- old"){if(!this.oldWebkit(n))return!1}else n.nodes=this.convertDirection(n.nodes),n.value=t+n.value;return i.toString()}replaceFirst(e,...t){return t.map(n=>n===" "?{type:"space",value:n}:{type:"word",value:n}).concat(e.slice(1))}normalizeUnit(e,t){return`${parseFloat(e)/t*360}deg`}normalize(e){if(!e[0])return e;if(/-?\d+(.\d+)?grad/.test(e[0].value))e[0].value=this.normalizeUnit(e[0].value,400);else if(/-?\d+(.\d+)?rad/.test(e[0].value))e[0].value=this.normalizeUnit(e[0].value,2*Math.PI);else if(/-?\d+(.\d+)?turn/.test(e[0].value))e[0].value=this.normalizeUnit(e[0].value,1);else if(e[0].value.includes("deg")){let t=parseFloat(e[0].value);t=yR.wrap(0,360,t),e[0].value=`${t}deg`}return e[0].value==="0deg"?e=this.replaceFirst(e,"to"," ","top"):e[0].value==="90deg"?e=this.replaceFirst(e,"to"," ","right"):e[0].value==="180deg"?e=this.replaceFirst(e,"to"," ","bottom"):e[0].value==="270deg"&&(e=this.replaceFirst(e,"to"," ","left")),e}newDirection(e){if(e[0].value==="to"||(j0.lastIndex=0,!j0.test(e[0].value)))return e;e.unshift({type:"word",value:"to"},{type:"space",value:" "});for(let t=2;t0&&(e[0].value==="to"?this.fixDirection(e):e[0].value.includes("deg")?this.fixAngle(e):this.isRadial(e)&&this.fixRadial(e)),e}fixDirection(e){e.splice(0,2);for(let t of e){if(t.type==="div")break;t.type==="word"&&(t.value=this.revertDirection(t.value))}}fixAngle(e){let t=e[0].value;t=parseFloat(t),t=Math.abs(450-t)%360,t=this.roundFloat(t,3),e[0].value=`${t}deg`}fixRadial(e){let t=[],i=[],n,s,a,o,l;for(o=0;o{u();var xR=xr(),kR=He();function V0(r){return new RegExp(`(^|[\\s,(])(${r}($|[\\s),]))`,"gi")}var Xu=class extends kR{regexp(){return this.regexpCache||(this.regexpCache=V0(this.name)),this.regexpCache}isStretch(){return this.name==="stretch"||this.name==="fill"||this.name==="fill-available"}replace(e,t){return t==="-moz-"&&this.isStretch()?e.replace(this.regexp(),"$1-moz-available$3"):t==="-webkit-"&&this.isStretch()?e.replace(this.regexp(),"$1-webkit-fill-available$3"):super.replace(e,t)}old(e){let t=e+this.name;return this.isStretch()&&(e==="-moz-"?t="-moz-available":e==="-webkit-"&&(t="-webkit-fill-available")),new xR(this.name,t,t,V0(t))}add(e,t){if(!(e.prop.includes("grid")&&t!=="-webkit-"))return super.add(e,t)}};Xu.names=["max-content","min-content","fit-content","fill","fill-available","stretch"];H0.exports=Xu});var Y0=x((bL,Q0)=>{u();var G0=xr(),SR=He(),Zu=class extends SR{replace(e,t){return t==="-webkit-"?e.replace(this.regexp(),"$1-webkit-optimize-contrast"):t==="-moz-"?e.replace(this.regexp(),"$1-moz-crisp-edges"):super.replace(e,t)}old(e){return e==="-webkit-"?new G0(this.name,"-webkit-optimize-contrast"):e==="-moz-"?new G0(this.name,"-moz-crisp-edges"):super.old(e)}};Zu.names=["pixelated"];Q0.exports=Zu});var X0=x((wL,K0)=>{u();var AR=He(),Ju=class extends AR{replace(e,t){let i=super.replace(e,t);return t==="-webkit-"&&(i=i.replace(/("[^"]+"|'[^']+')(\s+\d+\w)/gi,"url($1)$2")),i}};Ju.names=["image-set"];K0.exports=Ju});var J0=x((vL,Z0)=>{u();var CR=$e().list,_R=He(),ef=class extends _R{replace(e,t){return CR.space(e).map(i=>{if(i.slice(0,+this.name.length+1)!==this.name+"(")return i;let n=i.lastIndexOf(")"),s=i.slice(n+1),a=i.slice(this.name.length+1,n);if(t==="-webkit-"){let o=a.match(/\d*.?\d+%?/);o?(a=a.slice(o[0].length).trim(),a+=`, ${o[0]}`):a+=", 0.5"}return t+this.name+"("+a+")"+s}).join(" ")}};ef.names=["cross-fade"];Z0.exports=ef});var tv=x((xL,ev)=>{u();var ER=Pe(),OR=xr(),TR=He(),tf=class extends TR{constructor(e,t){super(e,t);e==="display-flex"&&(this.name="flex")}check(e){return e.prop==="display"&&e.value===this.name}prefixed(e){let t,i;return[t,e]=ER(e),t===2009?this.name==="flex"?i="box":i="inline-box":t===2012?this.name==="flex"?i="flexbox":i="inline-flexbox":t==="final"&&(i=this.name),e+i}replace(e,t){return this.prefixed(t)}old(e){let t=this.prefixed(e);if(!!t)return new OR(this.name,t)}};tf.names=["display-flex","inline-flex"];ev.exports=tf});var iv=x((kL,rv)=>{u();var RR=He(),rf=class extends RR{constructor(e,t){super(e,t);e==="display-grid"&&(this.name="grid")}check(e){return e.prop==="display"&&e.value===this.name}};rf.names=["display-grid","inline-grid"];rv.exports=rf});var sv=x((SL,nv)=>{u();var PR=He(),nf=class extends PR{constructor(e,t){super(e,t);e==="filter-function"&&(this.name="filter")}};nf.names=["filter","filter-function"];nv.exports=nf});var uv=x((AL,lv)=>{u();var av=Ni(),z=j(),ov=zy(),IR=ab(),DR=ou(),qR=Cb(),sf=Mt(),Pr=kr(),$R=Db(),ut=He(),Ir=_e(),LR=$b(),MR=Mb(),NR=Bb(),BR=jb(),FR=Wb(),jR=Yb(),zR=Xb(),UR=Jb(),VR=tw(),HR=iw(),WR=sw(),GR=ow(),QR=uw(),YR=cw(),KR=dw(),XR=gw(),ZR=bw(),JR=xw(),e5=Sw(),t5=Cw(),r5=Ow(),i5=Rw(),n5=Dw(),s5=$w(),a5=Mw(),o5=Bw(),l5=jw(),u5=Vw(),f5=Ww(),c5=Qw(),p5=Kw(),d5=Zw(),h5=e0(),m5=r0(),g5=s0(),y5=o0(),b5=u0(),w5=c0(),v5=d0(),x5=g0(),k5=b0(),S5=v0(),A5=S0(),C5=C0(),_5=E0(),E5=R0(),O5=I0(),T5=q0(),R5=U0(),P5=W0(),I5=Y0(),D5=X0(),q5=J0(),$5=tv(),L5=iv(),M5=sv();Pr.hack(LR);Pr.hack(MR);Pr.hack(NR);Pr.hack(BR);z.hack(FR);z.hack(jR);z.hack(zR);z.hack(UR);z.hack(VR);z.hack(HR);z.hack(WR);z.hack(GR);z.hack(QR);z.hack(YR);z.hack(KR);z.hack(XR);z.hack(ZR);z.hack(JR);z.hack(e5);z.hack(t5);z.hack(r5);z.hack(i5);z.hack(n5);z.hack(s5);z.hack(a5);z.hack(o5);z.hack(l5);z.hack(u5);z.hack(f5);z.hack(c5);z.hack(p5);z.hack(d5);z.hack(h5);z.hack(m5);z.hack(g5);z.hack(y5);z.hack(b5);z.hack(w5);z.hack(v5);z.hack(x5);z.hack(k5);z.hack(S5);z.hack(A5);z.hack(C5);z.hack(_5);z.hack(E5);z.hack(O5);z.hack(T5);ut.hack(R5);ut.hack(P5);ut.hack(I5);ut.hack(D5);ut.hack(q5);ut.hack($5);ut.hack(L5);ut.hack(M5);var af=new Map,Fi=class{constructor(e,t,i={}){this.data=e,this.browsers=t,this.options=i,[this.add,this.remove]=this.preprocess(this.select(this.data)),this.transition=new IR(this),this.processor=new DR(this)}cleaner(){if(this.cleanerCache)return this.cleanerCache;if(this.browsers.selected.length){let e=new sf(this.browsers.data,[]);this.cleanerCache=new Fi(this.data,e,this.options)}else return this;return this.cleanerCache}select(e){let t={add:{},remove:{}};for(let i in e){let n=e[i],s=n.browsers.map(l=>{let c=l.split(" ");return{browser:`${c[0]} ${c[1]}`,note:c[2]}}),a=s.filter(l=>l.note).map(l=>`${this.browsers.prefix(l.browser)} ${l.note}`);a=Ir.uniq(a),s=s.filter(l=>this.browsers.isSelected(l.browser)).map(l=>{let c=this.browsers.prefix(l.browser);return l.note?`${c} ${l.note}`:c}),s=this.sort(Ir.uniq(s)),this.options.flexbox==="no-2009"&&(s=s.filter(l=>!l.includes("2009")));let o=n.browsers.map(l=>this.browsers.prefix(l));n.mistakes&&(o=o.concat(n.mistakes)),o=o.concat(a),o=Ir.uniq(o),s.length?(t.add[i]=s,s.length!s.includes(l)))):t.remove[i]=o}return t}sort(e){return e.sort((t,i)=>{let n=Ir.removeNote(t).length,s=Ir.removeNote(i).length;return n===s?i.length-t.length:s-n})}preprocess(e){let t={selectors:[],"@supports":new qR(Fi,this)};for(let n in e.add){let s=e.add[n];if(n==="@keyframes"||n==="@viewport")t[n]=new $R(n,s,this);else if(n==="@resolution")t[n]=new ov(n,s,this);else if(this.data[n].selector)t.selectors.push(Pr.load(n,s,this));else{let a=this.data[n].props;if(a){let o=ut.load(n,s,this);for(let l of a)t[l]||(t[l]={values:[]}),t[l].values.push(o)}else{let o=t[n]&&t[n].values||[];t[n]=z.load(n,s,this),t[n].values=o}}}let i={selectors:[]};for(let n in e.remove){let s=e.remove[n];if(this.data[n].selector){let a=Pr.load(n,s);for(let o of s)i.selectors.push(a.old(o))}else if(n==="@keyframes"||n==="@viewport")for(let a of s){let o=`@${a}${n.slice(1)}`;i[o]={remove:!0}}else if(n==="@resolution")i[n]=new ov(n,s,this);else{let a=this.data[n].props;if(a){let o=ut.load(n,[],this);for(let l of s){let c=o.old(l);if(c)for(let f of a)i[f]||(i[f]={}),i[f].values||(i[f].values=[]),i[f].values.push(c)}}else for(let o of s){let l=this.decl(n).old(n,o);if(n==="align-self"){let c=t[n]&&t[n].prefixes;if(c){if(o==="-webkit- 2009"&&c.includes("-webkit-"))continue;if(o==="-webkit-"&&c.includes("-webkit- 2009"))continue}}for(let c of l)i[c]||(i[c]={}),i[c].remove=!0}}}return[t,i]}decl(e){return af.has(e)||af.set(e,z.load(e)),af.get(e)}unprefixed(e){let t=this.normalize(av.unprefixed(e));return t==="flex-direction"&&(t="flex-flow"),t}normalize(e){return this.decl(e).normalize(e)}prefixed(e,t){return e=av.unprefixed(e),this.decl(e).prefixed(e,t)}values(e,t){let i=this[e],n=i["*"]&&i["*"].values,s=i[t]&&i[t].values;return n&&s?Ir.uniq(n.concat(s)):n||s||[]}group(e){let t=e.parent,i=t.index(e),{length:n}=t.nodes,s=this.unprefixed(e.prop),a=(o,l)=>{for(i+=o;i>=0&&i{u();fv.exports={"backdrop-filter":{feature:"css-backdrop-filter",browsers:["ios_saf 16.1","ios_saf 16.3","ios_saf 16.4","ios_saf 16.5","safari 16.5"]},element:{props:["background","background-image","border-image","mask","list-style","list-style-image","content","mask-image"],feature:"css-element-function",browsers:["firefox 114"]},"user-select":{mistakes:["-khtml-"],feature:"user-select-none",browsers:["ios_saf 16.1","ios_saf 16.3","ios_saf 16.4","ios_saf 16.5","safari 16.5"]},"background-clip":{feature:"background-clip-text",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},hyphens:{feature:"css-hyphens",browsers:["ios_saf 16.1","ios_saf 16.3","ios_saf 16.4","ios_saf 16.5","safari 16.5"]},fill:{props:["width","min-width","max-width","height","min-height","max-height","inline-size","min-inline-size","max-inline-size","block-size","min-block-size","max-block-size","grid","grid-template","grid-template-rows","grid-template-columns","grid-auto-columns","grid-auto-rows"],feature:"intrinsic-width",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"fill-available":{props:["width","min-width","max-width","height","min-height","max-height","inline-size","min-inline-size","max-inline-size","block-size","min-block-size","max-block-size","grid","grid-template","grid-template-rows","grid-template-columns","grid-auto-columns","grid-auto-rows"],feature:"intrinsic-width",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},stretch:{props:["width","min-width","max-width","height","min-height","max-height","inline-size","min-inline-size","max-inline-size","block-size","min-block-size","max-block-size","grid","grid-template","grid-template-rows","grid-template-columns","grid-auto-columns","grid-auto-rows"],feature:"intrinsic-width",browsers:["firefox 114"]},"fit-content":{props:["width","min-width","max-width","height","min-height","max-height","inline-size","min-inline-size","max-inline-size","block-size","min-block-size","max-block-size","grid","grid-template","grid-template-rows","grid-template-columns","grid-auto-columns","grid-auto-rows"],feature:"intrinsic-width",browsers:["firefox 114"]},"text-decoration-style":{feature:"text-decoration",browsers:["ios_saf 16.1","ios_saf 16.3","ios_saf 16.4","ios_saf 16.5"]},"text-decoration-color":{feature:"text-decoration",browsers:["ios_saf 16.1","ios_saf 16.3","ios_saf 16.4","ios_saf 16.5"]},"text-decoration-line":{feature:"text-decoration",browsers:["ios_saf 16.1","ios_saf 16.3","ios_saf 16.4","ios_saf 16.5"]},"text-decoration":{feature:"text-decoration",browsers:["ios_saf 16.1","ios_saf 16.3","ios_saf 16.4","ios_saf 16.5"]},"text-decoration-skip":{feature:"text-decoration",browsers:["ios_saf 16.1","ios_saf 16.3","ios_saf 16.4","ios_saf 16.5"]},"text-decoration-skip-ink":{feature:"text-decoration",browsers:["ios_saf 16.1","ios_saf 16.3","ios_saf 16.4","ios_saf 16.5"]},"text-size-adjust":{feature:"text-size-adjust",browsers:["ios_saf 16.1","ios_saf 16.3","ios_saf 16.4","ios_saf 16.5"]},"mask-clip":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"mask-composite":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"mask-image":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"mask-origin":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"mask-repeat":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"mask-border-repeat":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"mask-border-source":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},mask:{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"mask-position":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"mask-size":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"mask-border":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"mask-border-outset":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"mask-border-width":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"mask-border-slice":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"clip-path":{feature:"css-clip-path",browsers:["samsung 21"]},"box-decoration-break":{feature:"css-boxdecorationbreak",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","ios_saf 16.1","ios_saf 16.3","ios_saf 16.4","ios_saf 16.5","opera 99","safari 16.5","samsung 21"]},appearance:{feature:"css-appearance",browsers:["samsung 21"]},"image-set":{props:["background","background-image","border-image","cursor","mask","mask-image","list-style","list-style-image","content"],feature:"css-image-set",browsers:["and_uc 15.5","chrome 109","samsung 21"]},"cross-fade":{props:["background","background-image","border-image","mask","list-style","list-style-image","content","mask-image"],feature:"css-cross-fade",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},isolate:{props:["unicode-bidi"],feature:"css-unicode-bidi",browsers:["ios_saf 16.1","ios_saf 16.3","ios_saf 16.4","ios_saf 16.5","safari 16.5"]},"color-adjust":{feature:"css-color-adjust",browsers:["chrome 109","chrome 113","chrome 114","edge 114","opera 99"]}}});var dv=x((_L,pv)=>{u();pv.exports={}});var yv=x((EL,gv)=>{u();var N5=Yl(),{agents:B5}=(Ps(),Rs),of=Oy(),F5=Mt(),j5=uv(),z5=cv(),U5=dv(),hv={browsers:B5,prefixes:z5},mv=` + Replace Autoprefixer \`browsers\` option to Browserslist config. + Use \`browserslist\` key in \`package.json\` or \`.browserslistrc\` file. + + Using \`browsers\` option can cause errors. Browserslist config can + be used for Babel, Autoprefixer, postcss-normalize and other tools. + + If you really need to use option, rename it to \`overrideBrowserslist\`. + + Learn more at: + https://github.com/browserslist/browserslist#readme + https://twitter.com/browserslist + +`;function V5(r){return Object.prototype.toString.apply(r)==="[object Object]"}var lf=new Map;function H5(r,e){e.browsers.selected.length!==0&&(e.add.selectors.length>0||Object.keys(e.add).length>2||r.warn(`Autoprefixer target browsers do not need any prefixes.You do not need Autoprefixer anymore. +Check your Browserslist config to be sure that your targets are set up correctly. + + Learn more at: + https://github.com/postcss/autoprefixer#readme + https://github.com/browserslist/browserslist#readme + +`))}gv.exports=Dr;function Dr(...r){let e;if(r.length===1&&V5(r[0])?(e=r[0],r=void 0):r.length===0||r.length===1&&!r[0]?r=void 0:r.length<=2&&(Array.isArray(r[0])||!r[0])?(e=r[1],r=r[0]):typeof r[r.length-1]=="object"&&(e=r.pop()),e||(e={}),e.browser)throw new Error("Change `browser` option to `overrideBrowserslist` in Autoprefixer");if(e.browserslist)throw new Error("Change `browserslist` option to `overrideBrowserslist` in Autoprefixer");e.overrideBrowserslist?r=e.overrideBrowserslist:e.browsers&&(typeof console!="undefined"&&console.warn&&(of.red?console.warn(of.red(mv.replace(/`[^`]+`/g,n=>of.yellow(n.slice(1,-1))))):console.warn(mv)),r=e.browsers);let t={ignoreUnknownVersions:e.ignoreUnknownVersions,stats:e.stats,env:e.env};function i(n){let s=hv,a=new F5(s.browsers,r,n,t),o=a.selected.join(", ")+JSON.stringify(e);return lf.has(o)||lf.set(o,new j5(s.prefixes,a,e)),lf.get(o)}return{postcssPlugin:"autoprefixer",prepare(n){let s=i({from:n.opts.from,env:e.env});return{OnceExit(a){H5(n,s),e.remove!==!1&&s.processor.remove(a,n),e.add!==!1&&s.processor.add(a,n)}}},info(n){return n=n||{},n.from=n.from||m.cwd(),U5(i(n))},options:e,browsers:r}}Dr.postcss=!0;Dr.data=hv;Dr.defaults=N5.defaults;Dr.info=()=>Dr().info()});var bv={};Ge(bv,{default:()=>W5});var W5,wv=P(()=>{u();W5=[]});var xv={};Ge(xv,{default:()=>G5});var vv,G5,kv=P(()=>{u();Xi();vv=pe(rn()),G5=St(vv.default.theme)});var Av={};Ge(Av,{default:()=>Q5});var Sv,Q5,Cv=P(()=>{u();Xi();Sv=pe(rn()),Q5=St(Sv.default)});u();"use strict";var Y5=vt(_y()),K5=vt($e()),X5=vt(yv()),Z5=vt((wv(),bv)),J5=vt((kv(),xv)),eP=vt((Cv(),Av)),tP=vt((Vs(),_f)),rP=vt((al(),sl)),iP=vt((sa(),sc));function vt(r){return r&&r.__esModule?r:{default:r}}console.warn("cdn.tailwindcss.com should not be used in production. To use Tailwind CSS in production, install it as a PostCSS plugin or use the Tailwind CLI: https://tailwindcss.com/docs/installation");var Ns="tailwind",uf="text/tailwindcss",_v="/template.html",Yt,Ev=!0,Ov=0,ff=new Set,cf,Tv="",Rv=(r=!1)=>({get(e,t){return(!r||t==="config")&&typeof e[t]=="object"&&e[t]!==null?new Proxy(e[t],Rv()):e[t]},set(e,t,i){return e[t]=i,(!r||t==="config")&&pf(!0),!0}});window[Ns]=new Proxy({config:{},defaultTheme:J5.default,defaultConfig:eP.default,colors:tP.default,plugin:rP.default,resolveConfig:iP.default},Rv(!0));function Pv(r){cf.observe(r,{attributes:!0,attributeFilter:["type"],characterData:!0,subtree:!0,childList:!0})}new MutationObserver(async r=>{let e=!1;if(!cf){cf=new MutationObserver(async()=>await pf(!0));for(let t of document.querySelectorAll(`style[type="${uf}"]`))Pv(t)}for(let t of r)for(let i of t.addedNodes)i.nodeType===1&&i.tagName==="STYLE"&&i.getAttribute("type")===uf&&(Pv(i),e=!0);await pf(e)}).observe(document.documentElement,{attributes:!0,attributeFilter:["class"],childList:!0,subtree:!0});async function pf(r=!1){r&&(Ov++,ff.clear());let e="";for(let i of document.querySelectorAll(`style[type="${uf}"]`))e+=i.textContent;let t=new Set;for(let i of document.querySelectorAll("[class]"))for(let n of i.classList)ff.has(n)||t.add(n);if(document.body&&(Ev||t.size>0||e!==Tv||!Yt||!Yt.isConnected)){for(let n of t)ff.add(n);Ev=!1,Tv=e,self[_v]=Array.from(t).join(" ");let{css:i}=await(0,K5.default)([(0,Y5.default)({...window[Ns].config,_hash:Ov,content:{files:[_v],extract:{html:n=>n.split(" ")}},plugins:[...Z5.default,...Array.isArray(window[Ns].config.plugins)?window[Ns].config.plugins:[]]}),(0,X5.default)({remove:!1})]).process(`@tailwind base;@tailwind components;@tailwind utilities;${e}`);(!Yt||!Yt.isConnected)&&(Yt=document.createElement("style"),document.head.append(Yt)),Yt.textContent=i}}})(); +/*! + * fill-range + * + * Copyright (c) 2014-present, Jon Schlinkert. + * Licensed under the MIT License. + */ +/*! + * is-number + * + * Copyright (c) 2014-present, Jon Schlinkert. + * Released under the MIT License. + */ +/*! + * to-regex-range + * + * Copyright (c) 2015-present, Jon Schlinkert. + * Released under the MIT License. + */ +/*! https://mths.be/cssesc v3.0.0 by @mathias */ diff --git a/src/internal/ui/static/workflow.js b/src/internal/ui/static/workflow.js new file mode 100644 index 0000000..6182e1d --- /dev/null +++ b/src/internal/ui/static/workflow.js @@ -0,0 +1,109 @@ +// Copyright 2026 Jordan Sherer +// SPDX-License-Identifier: gpl + +function addParam(btn) { + const stepRow = btn.closest('.step-row'); + const stepIdx = stepRow.dataset.index; + const container = btn.previousElementSibling; + const row = document.createElement('div'); + row.className = 'param-row flex gap-1'; + row.innerHTML = ` + + + + `; + container.appendChild(row); +} + +function reindexSteps() { + const list = document.getElementById('steps-list'); + const rows = list.querySelectorAll('.step-row'); + rows.forEach((r, i) => { + r.dataset.index = i; + const num = r.querySelector('.step-num'); + if (num) num.textContent = i + 1; + r.querySelectorAll('[name^="step_tool_"]').forEach(el => el.name = 'step_tool_' + i); + r.querySelectorAll('[name^="step_store_"]').forEach(el => el.name = 'step_store_' + i); + r.querySelectorAll('[name^="step_if_"]').forEach(el => el.name = 'step_if_' + i); + r.querySelectorAll('[name^="step_require_"]').forEach(el => el.name = 'step_require_' + i); + r.querySelectorAll('[name^="step_param_key_"]').forEach(el => el.name = 'step_param_key_' + i + '[]'); + r.querySelectorAll('[name^="step_param_val_"]').forEach(el => el.name = 'step_param_val_' + i + '[]'); + const params = r.querySelector('.step-params'); + if (params) params.dataset.index = i; + }); +} + +function moveStep(btn, direction) { + const row = btn.closest('.step-row'); + const list = document.getElementById('steps-list'); + const rows = Array.from(list.querySelectorAll('.step-row')); + const idx = rows.indexOf(row); + const targetIdx = idx + direction; + if (targetIdx < 0 || targetIdx >= rows.length) return; + + const other = rows[targetIdx]; + swapStepValues(row, other); + updateStepSummary(row); + updateStepSummary(other); +} + +function swapStepValues(rowA, rowB) { + const selA = rowA.querySelector('.step-tool'); + const selB = rowB.querySelector('.step-tool'); + if (selA && selB) { + const tmp = selA.value; + selA.value = selB.value; + selB.value = tmp; + } + + const inputsA = rowA.querySelectorAll('input[type="text"]'); + const inputsB = rowB.querySelectorAll('input[type="text"]'); + const coreInputsA = Array.from(inputsA).filter(el => !el.name.includes('param')); + const coreInputsB = Array.from(inputsB).filter(el => !el.name.includes('param')); + + for (let i = 0; i < Math.min(coreInputsA.length, coreInputsB.length); i++) { + const tmp = coreInputsA[i].value; + coreInputsA[i].value = coreInputsB[i].value; + coreInputsB[i].value = tmp; + } + + const paramsA = rowA.querySelector('.step-params'); + const paramsB = rowB.querySelector('.step-params'); + if (paramsA && paramsB) { + const tmpHTML = paramsA.innerHTML; + paramsA.innerHTML = paramsB.innerHTML; + paramsB.innerHTML = tmpHTML; + } + + reindexSteps(); +} + +function updateStepSummary(row) { + const sel = row.querySelector('.step-tool'); + const summaryTool = row.querySelector('summary .font-mono'); + if (sel && summaryTool) { + summaryTool.textContent = sel.value || 'new step'; + } + const storeInput = row.querySelector('input[name^="step_store_"]'); + const storeSpan = row.querySelector('summary .text-indigo-600'); + const arrowSpan = storeSpan ? storeSpan.previousElementSibling : null; + if (storeInput && storeSpan) { + if (storeInput.value) { + storeSpan.textContent = storeInput.value; + storeSpan.classList.remove('hidden'); + if (arrowSpan) arrowSpan.classList.remove('hidden'); + } else { + storeSpan.classList.add('hidden'); + if (arrowSpan) arrowSpan.classList.add('hidden'); + } + } +} + +function removeStep(btn) { + const row = btn.closest('.step-row'); + row.remove(); + reindexSteps(); + if (document.querySelectorAll('.step-row').length === 0) { + document.getElementById('steps-list').innerHTML = '
No steps. Click "+ Add" to start.
'; + } +} diff --git a/src/internal/ui/templates/auth.html b/src/internal/ui/templates/auth.html new file mode 100644 index 0000000..38aa918 --- /dev/null +++ b/src/internal/ui/templates/auth.html @@ -0,0 +1,165 @@ + + +{{define "content"}} +
+

Auth Providers

+ +
+ + + + +{{if not .Providers}} +
+

No OAuth providers configured

+

Click "+ New Provider" above to add an OAuth provider.

+
+{{else}} + +
+
+ Providers +
+
+ {{range .Providers}} +
+
+
+ + {{if eq .Status "valid"}}✓{{else if eq .Status "expired_refreshable"}}⟳{{else}}✗{{end}} + + {{.Name}} + {{.StatusLabel}} +
+
+ {{if not .HasToken}} + factorly auth login {{.Name}} + {{end}} + {{if eq .Status "expired_refreshable"}} + + {{end}} + {{if .HasToken}} + + {{end}} + + +
+
+ + {{if .Tools}} +
+ {{range .Tools}} + {{.}} + {{end}} +
+ {{end}} + + {{if .Scopes}} +
+ {{range .Scopes}} + {{.}} + {{end}} +
+ {{end}} + + + +
+ {{end}} +
+
+ +{{end}} +{{end}} diff --git a/src/internal/ui/templates/history.html b/src/internal/ui/templates/history.html new file mode 100644 index 0000000..20d63ac --- /dev/null +++ b/src/internal/ui/templates/history.html @@ -0,0 +1,96 @@ + + +{{define "content"}} +
+

History

+
+ + +
+
+ +{{if not .Entries}} +
+

No history yet

+

Tool calls will appear here after you run them.

+
+{{else}} +
+ {{range .Entries}} +
+ +
+ + {{if eq .Status "success"}}✓{{else if eq .Status "blocked"}}✗{{else}}!{{end}} + + {{.Tool}} + {{if .ShadowAction}} + {{.ShadowAction}} + {{end}} +
+
+ {{.DurationMs}}ms + {{.TimestampRel}} +
+
+
+
+
Time
{{.Timestamp}}
+
Interface
{{.Interface}}
+
Status
{{.Status}}
+
Duration
{{.DurationMs}}ms
+ {{if .AgentID}}
Agent
{{.AgentID}}
{{end}} + {{if .ShadowAction}}
Shadow
{{.ShadowAction}}
{{end}} +
+ + {{if .Params}} +
+

Parameters

+
+ {{range $k, $v := .Params}} +
+
{{$k}}
+
{{$v}}
+
+ {{end}} +
+
+ {{end}} + + {{if .Error}} +
+
{{.Error}}
+
+ {{end}} + + {{if .Output}} +
+
{{.Output}}
+
+ {{end}} +
+
+ {{end}} +
+{{end}} +{{end}} diff --git a/src/internal/ui/templates/import.html b/src/internal/ui/templates/import.html new file mode 100644 index 0000000..1037ce9 --- /dev/null +++ b/src/internal/ui/templates/import.html @@ -0,0 +1,32 @@ + + +{{define "content"}} +

Import from OpenAPI

+ +
+
+

Provide an OpenAPI 3.x spec URL or file path to generate REST tool definitions automatically.

+
+
+ + +
+
+ + +

Tools will be named prefix.operation_id (e.g., github.list_repos)

+
+ +
+
+ +
+
+{{end}} diff --git a/src/internal/ui/templates/layout.html b/src/internal/ui/templates/layout.html new file mode 100644 index 0000000..6e6280e --- /dev/null +++ b/src/internal/ui/templates/layout.html @@ -0,0 +1,266 @@ + + +{{define "layout"}} + + + + + {{if .Title}}{{.Title}} — {{end}}Factorly + + + + + + + + + + +
+ {{if eq .Nav "tools"}} + + + {{else if eq .Nav "workflows"}} + + + {{end}} + + +
+
+ {{template "content" .}} +
+
+ + +
+
+
+ Activity + + + live + +
+
+ 0 calls + + +
+
+
+
+ Waiting for agent activity... +
+
+
+
+ + + + + + + + + + + + + +{{end}} diff --git a/src/internal/ui/templates/partials/empty.html b/src/internal/ui/templates/partials/empty.html new file mode 100644 index 0000000..a39ce1d --- /dev/null +++ b/src/internal/ui/templates/partials/empty.html @@ -0,0 +1,6 @@ + + +{{define "empty"}}{{end}} diff --git a/src/internal/ui/templates/partials/import_preview.html b/src/internal/ui/templates/partials/import_preview.html new file mode 100644 index 0000000..8053ecb --- /dev/null +++ b/src/internal/ui/templates/partials/import_preview.html @@ -0,0 +1,41 @@ +{{define "import_error"}} +
{{.Message}}
+{{end}} + +{{define "import_warning"}} +
{{.Message}}
+{{end}} + +{{define "import_preview"}} +
+
+ +
+
+ +
+
+ + +
+ {{range .Tools}} + + {{end}} +
+
+ + Uncheck tools you don't want to import. +
+
+
+{{end}} diff --git a/src/internal/ui/templates/partials/tool_form_fields.html b/src/internal/ui/templates/partials/tool_form_fields.html new file mode 100644 index 0000000..62e3b3d --- /dev/null +++ b/src/internal/ui/templates/partials/tool_form_fields.html @@ -0,0 +1,91 @@ +{{define "tool_form_cli"}} +
+
+ + +
+
+ + +

Space-separated. Use {{"{{param}}"}} for placeholders.

+
+
+{{end}} + +{{define "tool_form_rest"}} +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +{{end}} + +{{define "tool_form_mcp"}} +
+
+ + +
+
+ + +

Space-separated arguments for the MCP server command.

+
+
+ + +

Leave empty for stdio transport (command-based).

+
+
+{{end}} diff --git a/src/internal/ui/templates/partials/try_panel.html b/src/internal/ui/templates/partials/try_panel.html new file mode 100644 index 0000000..5c2398c --- /dev/null +++ b/src/internal/ui/templates/partials/try_panel.html @@ -0,0 +1,46 @@ +{{define "try_panel"}} +
+
+

Try it

+ {{.Name}} +
+ +
+ {{if .Params}} +
+ {{range .Params}} +
+ + {{if .Description}}

{{.Description}}

{{end}} + {{if eq .Type "text"}} + + {{else}} + + {{end}} +
+ {{end}} +
+ {{else}} +

No parameters.

+ {{end}} + + + + executing... + +
+ +
+
+{{end}} diff --git a/src/internal/ui/templates/partials/try_result.html b/src/internal/ui/templates/partials/try_result.html new file mode 100644 index 0000000..7aff854 --- /dev/null +++ b/src/internal/ui/templates/partials/try_result.html @@ -0,0 +1,130 @@ +{{define "try_blocked"}} +
+
+
+ + blocked + {{.Tool}} +
+ {{.DurationMs}}ms +
+
+
{{.Error}}
+
+
+{{end}} + +{{define "try_success"}} +
+
+
+ {{.Icon}} + {{.Status}} + {{.Tool}} +
+
+ {{.DurationMs}}ms + {{.OutputSize}}b +
+
+ +
+
+ + + +
+
+ {{if .IsJSON}} + + + {{end}} + +
+
+ +
+
+
{{.FormattedOutput}}
+
+ + +
+
+ + +{{end}} diff --git a/src/internal/ui/templates/partials/workflow_step.html b/src/internal/ui/templates/partials/workflow_step.html new file mode 100644 index 0000000..85ac19f --- /dev/null +++ b/src/internal/ui/templates/partials/workflow_step.html @@ -0,0 +1,46 @@ +{{define "workflow_step"}} +
+ + {{inc .Index}} + new step + + + + + + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+
+
+{{end}} diff --git a/src/internal/ui/templates/template_detail.html b/src/internal/ui/templates/template_detail.html new file mode 100644 index 0000000..a790c1a --- /dev/null +++ b/src/internal/ui/templates/template_detail.html @@ -0,0 +1,59 @@ + + +{{define "content"}} + + +
+

{{.Template.DisplayName}}

+ {{.Template.Category}} +
+

{{.Template.Description}}

+ +
+ +
+

Tools included

+
+ {{range .ToolNames}} +
{{.}}
+ {{end}} +
+
+ + +
+

Authentication

+
+ {{.Template.AuthType}} + {{if .Template.VaultKey}} + vault key: {{.Template.VaultKey}} + {{end}} +
+ {{if .Template.AuthGuide}} +
{{.Template.AuthGuide}}
+ {{end}} +
+
+ + +
+
+ + Adds tools to your project config +
+
+ + +
+

YAML

+
{{.YAML}}
+
+{{end}} diff --git a/src/internal/ui/templates/templates.html b/src/internal/ui/templates/templates.html new file mode 100644 index 0000000..8d02ab4 --- /dev/null +++ b/src/internal/ui/templates/templates.html @@ -0,0 +1,22 @@ + + +{{define "content"}} +

Templates

+

Pre-built tool configurations. Pick one to add to your project.

+ + +{{end}} diff --git a/src/internal/ui/templates/tool_edit.html b/src/internal/ui/templates/tool_edit.html new file mode 100644 index 0000000..691e4b6 --- /dev/null +++ b/src/internal/ui/templates/tool_edit.html @@ -0,0 +1,532 @@ + + +{{define "content"}} + +
+
+ +
+
+

{{.Tool.Name}}

+ {{if eq .Tool.Type "cli"}} + CLI + {{else if eq .Tool.Type "rest"}} + REST + {{else if eq .Tool.Type "mcp"}} + MCP + {{else if eq .Tool.Type "builtin"}} + Built-in + {{end}} + {{if not .IsBuiltin}} + + {{end}} +
+ {{if .Tool.Description}}

{{.Tool.Description}}

{{end}} +
+ + +
+ {{if not .IsBuiltin}} +
+ + +
+ {{end}} +
+ +{{if .Tool.Hidden}} +
+ {{icon "lock"}} + Hidden from MCP listing and tools list. Still callable directly and by workflows. +
+{{end}} + +{{if .Tool.Shadow}} +
+ {{icon "shield"}} + Oversight: + {{shadowSummary .Tool.Shadow}} +
+{{end}} + +
+ +
+
+ + {{if .IsBuiltin}} + +
+ +
+

Built-in tool

+

Executes in-process. Configuration is managed by Factorly. Oversight settings are editable below.

+
+
+ {{else if eq .Tool.Type "cli"}} + +
+
+ + + +
+
$ {{.Tool.Command}} {{joinArgs .Tool.Args}}
+
+ {{else if eq .Tool.Type "rest"}} + +
+
+ {{.Tool.Method}} + {{.Tool.BaseURL}}{{.Tool.Path}} +
+
+ {{end}} + + {{if not .IsBuiltin}} + +
+
+ Configuration +
+
+ + + {{if eq .Tool.Type "cli"}} +
+ + +
+
+ + +
+
+ + +
+ + {{else if eq .Tool.Type "rest"}} +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+ {{if and .Tool.Auth (eq .Tool.Auth.Type "bearer")}} + + {{else if and .Tool.Auth (eq .Tool.Auth.Type "header")}} +
+ + +
+ {{else if and .Tool.Auth (eq .Tool.Auth.Type "oauth")}} + + {{end}} +
+
+ +
+ +
+ {{range $k, $v := .Tool.Headers}} +
+ + + +
+ {{end}} +
+ +
+ +
+
+ + +
+
+ + +
+
+ + {{else if eq .Tool.Type "mcp"}} +
+ + +
+
+ + +
+ {{end}} +
+
+ {{end}}{{/* end if not .IsBuiltin */}} + + +
+
+ + + + Parameters {{if .Params}}({{len .Params}}){{end}} + + {{if not .IsBuiltin}}{{icon "plus"}} Add{{end}} + +
+ {{range $i, $p := .Params}} +
+
+
+ + + + +
+ +
+
+ + +
+
+ {{end}} +
+
+
+ + +
+
+ + + Oversight + +
+
+ + +

Block calls where any parameter value contains these strings. The tool call will be rejected immediately.

+
+
+ +
+
+ + +
+
+
+
+ + +
+
+ + + Output Filter + +
+
+ + +

Extract a specific field from JSON output.

+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +

Only keep lines matching these patterns.

+
+
+ + +

Remove lines matching these patterns.

+
+
+ + +

Regex pattern→replacement, one per line. Use → (arrow) as separator.

+
+
+
+
+ + +
+
+ + + Advanced + +
+
+
+ + +
+
+ + +
+
+
+ + +
+ +
+
+
+ +
+ + +
+
+
+ + +
+
+
+

Try it

+ {{.Tool.Name}} +
+ +
+ + {{if .Params}} +
+ {{range .Params}} +
+ + {{if .Description}} +

{{.Description}}

+ {{end}} + +
+ {{end}} +
+ {{else}} +

No parameters.

+ {{end}} + + + + running... + +
+ +
+
+
+
+ + +{{end}} diff --git a/src/internal/ui/templates/tool_new.html b/src/internal/ui/templates/tool_new.html new file mode 100644 index 0000000..ada63f7 --- /dev/null +++ b/src/internal/ui/templates/tool_new.html @@ -0,0 +1,67 @@ + + +{{define "content"}} + + +

New Tool

+ +
+
+
+ + +

Dot-separated name (e.g., github.list_repos, slack.post_message)

+
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+ + +
+
+ + +

Space-separated. Use {{"{{param}}"}} for placeholders.

+
+
+
+ +
+ +
+
+
+{{end}} diff --git a/src/internal/ui/templates/tools.html b/src/internal/ui/templates/tools.html new file mode 100644 index 0000000..8160597 --- /dev/null +++ b/src/internal/ui/templates/tools.html @@ -0,0 +1,27 @@ + + +{{define "content"}} +{{if not .Tools}} +
+

No tools configured yet.

+ +
+{{else}} +
+

← Select a tool

+

Choose a tool from the sidebar to view its configuration and try it.

+ +
+{{end}} +{{end}} diff --git a/src/internal/ui/templates/vault.html b/src/internal/ui/templates/vault.html new file mode 100644 index 0000000..106574b --- /dev/null +++ b/src/internal/ui/templates/vault.html @@ -0,0 +1,102 @@ + + +{{define "content"}} +

Vault

+ +{{if not .Sections}} +
+ Vault not available. Run factorly vault set KEY value from the CLI to initialize. +
+{{else}} + + +
+

Add Secret

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ + +
+
+ {{range .Sections}} +
+
{{.Label}} ({{len .Keys}} keys)
+ {{if not .Keys}} +
empty
+ {{else}} + {{range .Keys}} +
+
+ {{.}} +
+ •••••••• + + +
+
+
+ {{end}} + {{end}} +
+ {{end}} +
+
+ + + +{{end}} +{{end}} diff --git a/src/internal/ui/templates/workflow_edit.html b/src/internal/ui/templates/workflow_edit.html new file mode 100644 index 0000000..a8f8868 --- /dev/null +++ b/src/internal/ui/templates/workflow_edit.html @@ -0,0 +1,398 @@ + + +{{define "content"}} +
+
+ +
+
+

{{.Name}}

+ workflow + +
+ {{if .Description}}

{{.Description}}

{{end}} +
+ + +
+ +
+ +
+ +
+
+ + + +
+
+ Parameters + +
+
+ {{if not .Params}} +

No input parameters. Agents will call this workflow without arguments.

+ {{end}} + {{range $i, $p := .Params}} +
+
+
+ + + +
+
+ + +
+
+ +
+ {{end}} +
+
+ +
+
+ Steps + +
+ +
+ {{if not .Steps}} +
+ No steps. Click "+ Add" to start. +
+ {{end}} + + {{range $i, $step := .Steps}} +
+ + {{inc $i}} + + {{if $step.Tool}} + {{$step.Tool}} + {{else}} + switch + {{end}} + + {{if $step.Store}} + + {{$step.Store}} + {{end}} + + {{if $step.If}}if{{end}} + {{if $step.Require}}req{{end}} + + + + + + + + +
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ +
+ {{range $k, $v := $step.Params}} +
+ + + +
+ {{end}} +
+ +
+
+
+ {{end}} +
+
+ + +
+
+ + + Oversight + +
+
+ + +

Block calls where any parameter value contains these strings. The tool call will be rejected immediately.

+
+
+ +
+
+ + +
+
+
+
+ + +
+
+ + + Output Filter + +
+
+ + +

Extract a specific field from JSON output.

+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +

Only keep lines matching these patterns.

+
+
+ + +

Remove lines matching these patterns.

+
+
+ + +

Regex pattern→replacement, one per line. Use → (arrow) as separator.

+
+
+
+
+ + +
+
+ + + Advanced + +
+
+
+ + +
+
+ + +
+
+
+ + +
+ +
+
+
+ +
+ +
+
+
+ + +
+
+
+

Try It

+ {{.Name}} +
+ +
+ + {{if .Params}} +
+ {{range .Params}} +
+ + {{if .Description}}

{{.Description}}

{{end}} + +
+ {{end}} +
+ {{else}} +

No parameters.

+ {{end}} + + + + executing... + +
+ +
+
+
+
+ + + +{{end}} diff --git a/src/internal/ui/templates/workflow_new.html b/src/internal/ui/templates/workflow_new.html new file mode 100644 index 0000000..52eb200 --- /dev/null +++ b/src/internal/ui/templates/workflow_new.html @@ -0,0 +1,32 @@ + + +{{define "content"}} +

New Workflow

+ +
+
+
+ + +

Dot-separated name (e.g., git.commit_prep, deploy.staging)

+
+ +
+ + +
+ +
+ +
+
+
+{{end}} diff --git a/src/internal/ui/templates/workflows.html b/src/internal/ui/templates/workflows.html new file mode 100644 index 0000000..135b7ff --- /dev/null +++ b/src/internal/ui/templates/workflows.html @@ -0,0 +1,20 @@ + + +{{define "content"}} +{{if not .SidebarWorkflows}} +
+

No workflows configured yet.

+

Workflows compose multiple tools into multi-step pipelines with conditionals and state.

+ {{icon "plus"}} Create New Workflow +
+{{else}} +
+

← Select a workflow

+

Choose a workflow from the sidebar to view its steps and run it.

+ {{icon "plus"}} Create New Workflow +
+{{end}} +{{end}} diff --git a/src/internal/ui/yamlio.go b/src/internal/ui/yamlio.go new file mode 100644 index 0000000..19a6db5 --- /dev/null +++ b/src/internal/ui/yamlio.go @@ -0,0 +1,445 @@ +// Copyright 2026 Jordan Sherer +// SPDX-License-Identifier: gpl + +package ui + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/factorly-dev/factorly/internal/config" + "gopkg.in/yaml.v3" +) + +// SaveTool writes a tool config to disk. If toolsDir is set, writes as +// individual file and removes any inline definition to avoid duplicates. +// Otherwise appends/updates in the main config file. +func SaveTool(cfgPath, toolsDir, name string, tc config.ToolConfig) error { + // Always clean from other locations to prevent duplicates + _ = deleteToolFromConfig(cfgPath, name) + removeToolFromLooseFiles(cfgPath, name) + if toolsDir != "" { + return saveToolToDir(toolsDir, name, tc) + } + return saveToolToConfig(cfgPath, name, tc) +} + +// DeleteTool removes a tool from disk (inline, loose files, and dir file). +func DeleteTool(cfgPath, toolsDir, name string) error { + _ = deleteToolFromConfig(cfgPath, name) + removeToolFromLooseFiles(cfgPath, name) + if toolsDir != "" { + return deleteToolFromDir(toolsDir, name) + } + return nil +} + +// removeToolFromLooseFiles removes a tool from any loose YAML files in the +// config directory (e.g., .factorly/*.yaml other than factorly.yaml). +func removeToolFromLooseFiles(cfgPath, name string) { + configDir := filepath.Dir(cfgPath) + configBase := filepath.Base(cfgPath) + + entries, err := os.ReadDir(configDir) + if err != nil { + return + } + for _, entry := range entries { + if entry.IsDir() { + continue + } + entryName := entry.Name() + // Skip the main config file and non-YAML files + if entryName == configBase { + continue + } + ext := filepath.Ext(entryName) + if ext != ".yaml" && ext != ".yml" { + continue + } + path := filepath.Join(configDir, entryName) + if findToolInFile(path, name) { + _ = removeToolFromFile(path, name) + } + } +} + +// findToolInFile checks if a YAML file contains a tool with the given name. +func findToolInFile(path, name string) bool { + data, err := os.ReadFile(path) + if err != nil { + return false + } + var tools map[string]any + if err := yaml.Unmarshal(data, &tools); err != nil { + return false + } + _, exists := tools[name] + return exists +} + +func saveToolToDir(toolsDir, name string, tc config.ToolConfig) error { + if err := os.MkdirAll(toolsDir, 0o755); err != nil { + return err + } + + // Find if tool exists in an existing file and update in-place + existingFile := findToolInDir(toolsDir, name) + if existingFile != "" { + return updateToolInFile(existingFile, name, tc) + } + + // Otherwise write a new file + filename, err := toolFilename(name) + if err != nil { + return err + } + path := filepath.Join(toolsDir, filename) + + data := map[string]config.ToolConfig{name: tc} + out, err := yaml.Marshal(data) + if err != nil { + return fmt.Errorf("marshaling tool: %w", err) + } + return os.WriteFile(path, out, 0o644) +} + +// findToolInDir returns the path of the file containing the named tool, or "". +func findToolInDir(toolsDir, name string) string { + entries, err := os.ReadDir(toolsDir) + if err != nil { + return "" + } + for _, entry := range entries { + if entry.IsDir() { + continue + } + ext := filepath.Ext(entry.Name()) + if ext != ".yaml" && ext != ".yml" { + continue + } + path := filepath.Join(toolsDir, entry.Name()) + data, err := os.ReadFile(path) + if err != nil { + continue + } + var tools map[string]any + if err := yaml.Unmarshal(data, &tools); err != nil { + continue + } + if _, exists := tools[name]; exists { + return path + } + } + return "" +} + +// updateToolInFile updates a single tool within a multi-tool YAML file, +// preserving other tools and field order. +func updateToolInFile(path, name string, tc config.ToolConfig) error { + data, err := os.ReadFile(path) + if err != nil { + return err + } + + // Marshal new tool config as a node + toolBytes, err := yaml.Marshal(tc) + if err != nil { + return err + } + var toolNode yaml.Node + if err := yaml.Unmarshal(toolBytes, &toolNode); err != nil { + return err + } + + // Parse existing file as node tree + var docNode yaml.Node + if err := yaml.Unmarshal(data, &docNode); err != nil { + return fmt.Errorf("parsing %s: %w", path, err) + } + + root := docNode.Content[0] // top-level mapping + for i := 0; i < len(root.Content)-1; i += 2 { + if root.Content[i].Value == name { + root.Content[i+1] = toolNode.Content[0] + break + } + } + + out, err := yaml.Marshal(&docNode) + if err != nil { + return err + } + return os.WriteFile(path, out, 0o644) +} + +func deleteToolFromDir(toolsDir, name string) error { + // First try the dedicated file + filename, err := toolFilename(name) + if err != nil { + return err + } + path := filepath.Join(toolsDir, filename) + if err := os.Remove(path); err == nil { + return nil + } + + // Otherwise find and remove from a multi-tool file + existingFile := findToolInDir(toolsDir, name) + if existingFile == "" { + return nil + } + return removeToolFromFile(existingFile, name) +} + +// removeToolFromFile removes a single tool from a multi-tool YAML file. +// If the file becomes empty, it is deleted. +func removeToolFromFile(path, name string) error { + data, err := os.ReadFile(path) + if err != nil { + return err + } + + var docNode yaml.Node + if err := yaml.Unmarshal(data, &docNode); err != nil { + return err + } + + root := docNode.Content[0] + var newContent []*yaml.Node + for i := 0; i < len(root.Content)-1; i += 2 { + if root.Content[i].Value == name { + continue // skip this tool + } + newContent = append(newContent, root.Content[i], root.Content[i+1]) + } + + if len(newContent) == 0 { + return os.Remove(path) + } + + root.Content = newContent + out, err := yaml.Marshal(&docNode) + if err != nil { + return err + } + return os.WriteFile(path, out, 0o644) +} + +func saveToolToConfig(cfgPath, name string, tc config.ToolConfig) error { + // Read existing config + data, err := os.ReadFile(cfgPath) + if err != nil { + return err + } + + // Marshal the tool config to a yaml.Node to preserve field order + toolBytes, err := yaml.Marshal(tc) + if err != nil { + return err + } + var toolNode yaml.Node + if err := yaml.Unmarshal(toolBytes, &toolNode); err != nil { + return err + } + + // Re-read the full file as a yaml.Node tree to preserve ordering + var docNode yaml.Node + if err := yaml.Unmarshal(data, &docNode); err != nil { + return fmt.Errorf("parsing config as node: %w", err) + } + + // Find or create the tools mapping node + root := docNode.Content[0] // document root mapping + var toolsNode *yaml.Node + for i := 0; i < len(root.Content)-1; i += 2 { + if root.Content[i].Value == "tools" { + toolsNode = root.Content[i+1] + break + } + } + if toolsNode == nil { + root.Content = append(root.Content, + &yaml.Node{Kind: yaml.ScalarNode, Value: "tools"}, + &yaml.Node{Kind: yaml.MappingNode}, + ) + toolsNode = root.Content[len(root.Content)-1] + } + + // Ensure block style for readable output + toolsNode.Style = 0 + + // Replace or append the tool entry + found := false + for i := 0; i < len(toolsNode.Content)-1; i += 2 { + if toolsNode.Content[i].Value == name { + toolsNode.Content[i+1] = toolNode.Content[0] + found = true + break + } + } + if !found { + toolsNode.Content = append(toolsNode.Content, + &yaml.Node{Kind: yaml.ScalarNode, Value: name}, + toolNode.Content[0], + ) + } + + out, err := yaml.Marshal(&docNode) + if err != nil { + return err + } + return os.WriteFile(cfgPath, out, 0o644) +} + +func deleteToolFromConfig(cfgPath, name string) error { + data, err := os.ReadFile(cfgPath) + if err != nil { + return err + } + + var raw map[string]any + if err := yaml.Unmarshal(data, &raw); err != nil { + return err + } + + tools, ok := raw["tools"].(map[string]any) + if !ok { + return nil + } + delete(tools, name) + + out, err := yaml.Marshal(raw) + if err != nil { + return err + } + return os.WriteFile(cfgPath, out, 0o644) +} + +// SaveOAuthProvider writes an OAuth provider config to the main config file. +func SaveOAuthProvider(cfgPath, name string, p config.OAuthProviderConfig) error { + return upsertConfigMapEntry(cfgPath, "oauth_providers", name, p) +} + +// DeleteOAuthProvider removes an OAuth provider from the main config file. +func DeleteOAuthProvider(cfgPath, name string) error { + return deleteConfigMapEntry(cfgPath, "oauth_providers", name) +} + +// upsertConfigMapEntry adds or updates an entry in a top-level mapping in a YAML config file. +func upsertConfigMapEntry(cfgPath, mapKey, entryName string, value any) error { + data, err := os.ReadFile(cfgPath) + if err != nil { + return err + } + + valBytes, err := yaml.Marshal(value) + if err != nil { + return err + } + var valNode yaml.Node + if err := yaml.Unmarshal(valBytes, &valNode); err != nil { + return err + } + + var docNode yaml.Node + if err := yaml.Unmarshal(data, &docNode); err != nil { + return fmt.Errorf("parsing config: %w", err) + } + + root := docNode.Content[0] + var mapNode *yaml.Node + for i := 0; i < len(root.Content)-1; i += 2 { + if root.Content[i].Value == mapKey { + mapNode = root.Content[i+1] + break + } + } + if mapNode == nil { + root.Content = append(root.Content, + &yaml.Node{Kind: yaml.ScalarNode, Value: mapKey}, + &yaml.Node{Kind: yaml.MappingNode}, + ) + mapNode = root.Content[len(root.Content)-1] + } + mapNode.Style = 0 + + found := false + for i := 0; i < len(mapNode.Content)-1; i += 2 { + if mapNode.Content[i].Value == entryName { + mapNode.Content[i+1] = valNode.Content[0] + found = true + break + } + } + if !found { + mapNode.Content = append(mapNode.Content, + &yaml.Node{Kind: yaml.ScalarNode, Value: entryName}, + valNode.Content[0], + ) + } + + out, err := yaml.Marshal(&docNode) + if err != nil { + return err + } + return os.WriteFile(cfgPath, out, 0o644) +} + +// deleteConfigMapEntry removes an entry from a top-level mapping in a YAML config file. +func deleteConfigMapEntry(cfgPath, mapKey, entryName string) error { + data, err := os.ReadFile(cfgPath) + if err != nil { + return err + } + + var docNode yaml.Node + if err := yaml.Unmarshal(data, &docNode); err != nil { + return err + } + + root := docNode.Content[0] + for i := 0; i < len(root.Content)-1; i += 2 { + if root.Content[i].Value == mapKey { + mapNode := root.Content[i+1] + var newContent []*yaml.Node + for j := 0; j < len(mapNode.Content)-1; j += 2 { + if mapNode.Content[j].Value == entryName { + continue + } + newContent = append(newContent, mapNode.Content[j], mapNode.Content[j+1]) + } + mapNode.Content = newContent + break + } + } + + out, err := yaml.Marshal(&docNode) + if err != nil { + return err + } + return os.WriteFile(cfgPath, out, 0o644) +} + +// safePath sanitizes a user-provided name for use as a single path component. +// It rejects traversal attempts (..), path separators, and empty/dot-only names. +func safePath(name string) (string, error) { + s := strings.TrimSpace(name) + if s == "" || s == "." || s == ".." { + return "", fmt.Errorf("invalid name: %q", name) + } + if strings.ContainsAny(s, "/\\") || strings.Contains(s, "..") { + return "", fmt.Errorf("invalid name: %q (contains path separator or traversal)", name) + } + return s, nil +} + +func toolFilename(name string) (string, error) { + safe, err := safePath(name) + if err != nil { + return "", err + } + return safe + ".yaml", nil +} diff --git a/src/internal/ui/yamlio_test.go b/src/internal/ui/yamlio_test.go new file mode 100644 index 0000000..8addbb5 --- /dev/null +++ b/src/internal/ui/yamlio_test.go @@ -0,0 +1,663 @@ +// Copyright 2026 Jordan Sherer +// SPDX-License-Identifier: gpl + +package ui + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/factorly-dev/factorly/internal/config" +) + +func TestSaveToolToDir_TypeFirst(t *testing.T) { + dir := t.TempDir() + + tc := config.ToolConfig{ + Type: "cli", + Description: "lists files", + Command: "ls", + Args: []string{"-la"}, + } + + if err := saveToolToDir(dir, "fs.list", tc); err != nil { + t.Fatal(err) + } + + data, err := os.ReadFile(filepath.Join(dir, "fs.list.yaml")) + if err != nil { + t.Fatal(err) + } + + lines := strings.Split(string(data), "\n") + // First line is the tool name key, second should be type + var typeLine string + for _, l := range lines { + trimmed := strings.TrimSpace(l) + if strings.HasPrefix(trimmed, "type:") { + typeLine = trimmed + break + } + // Skip the tool name line + if strings.HasSuffix(trimmed, ":") || trimmed == "" { + continue + } + // If we hit a non-type field first, fail + t.Fatalf("expected 'type:' as first field, got: %q", trimmed) + } + + if typeLine != "type: cli" { + t.Fatalf("expected 'type: cli', got: %q", typeLine) + } +} + +func TestSaveToolToConfig_TypeFirst(t *testing.T) { + dir := t.TempDir() + cfgPath := filepath.Join(dir, "factorly.yaml") + + // Write a minimal config + initial := []byte("tools: {}\n") + if err := os.WriteFile(cfgPath, initial, 0o644); err != nil { + t.Fatal(err) + } + + tc := config.ToolConfig{ + Type: "rest", + Description: "fetch data", + Method: "GET", + BaseURL: "https://api.example.com", + Path: "/data", + } + + if err := saveToolToConfig(cfgPath, "api.fetch", tc); err != nil { + t.Fatal(err) + } + + data, err := os.ReadFile(cfgPath) + if err != nil { + t.Fatal(err) + } + + // Find the tool section and verify type comes first + content := string(data) + toolIdx := strings.Index(content, "api.fetch:") + if toolIdx == -1 { + t.Fatal("tool 'api.fetch' not found in output") + } + + afterTool := content[toolIdx:] + lines := strings.Split(afterTool, "\n") + // Skip the "api.fetch:" line itself + for _, l := range lines[1:] { + trimmed := strings.TrimSpace(l) + if trimmed == "" { + continue + } + if strings.HasPrefix(trimmed, "type:") { + if trimmed != "type: rest" { + t.Fatalf("expected 'type: rest', got: %q", trimmed) + } + return + } + t.Fatalf("expected 'type:' as first field under tool, got: %q", trimmed) + } + t.Fatal("'type:' field not found") +} + +func TestSaveToolToDir_NoDuplicate(t *testing.T) { + dir := t.TempDir() + cfgPath := filepath.Join(dir, "factorly.yaml") + toolsDir := filepath.Join(dir, "tools") + + // Write config with an inline tool + initial := []byte(`tools: + my.tool: + type: cli + command: echo +`) + if err := os.WriteFile(cfgPath, initial, 0o644); err != nil { + t.Fatal(err) + } + + tc := config.ToolConfig{ + Type: "cli", + Description: "updated", + Command: "echo", + Args: []string{"hello"}, + } + + // SaveTool should remove from inline and write to dir + if err := SaveTool(cfgPath, toolsDir, "my.tool", tc); err != nil { + t.Fatal(err) + } + + // Verify inline no longer has the tool + data, err := os.ReadFile(cfgPath) + if err != nil { + t.Fatal(err) + } + if strings.Contains(string(data), "my.tool") { + t.Fatal("tool should have been removed from inline config") + } + + // Verify dir file exists + dirFile := filepath.Join(toolsDir, "my.tool.yaml") + if _, err := os.Stat(dirFile); err != nil { + t.Fatalf("expected tool file in dir: %v", err) + } +} + +func TestSaveToolToDir_MultiToolFile(t *testing.T) { + toolsDir := t.TempDir() + + // Write a multi-tool file + multiFile := filepath.Join(toolsDir, "git.yaml") + initial := []byte(`git.log: + type: cli + command: git + args: + - log +git.status: + type: cli + command: git + args: + - status +`) + if err := os.WriteFile(multiFile, initial, 0o644); err != nil { + t.Fatal(err) + } + + // Update git.log — should update in-place in git.yaml, not create git.log.yaml + tc := config.ToolConfig{ + Type: "cli", + Command: "git", + Args: []string{"log", "--oneline"}, + } + if err := saveToolToDir(toolsDir, "git.log", tc); err != nil { + t.Fatal(err) + } + + // Should NOT have created a new file + if _, err := os.Stat(filepath.Join(toolsDir, "git.log.yaml")); err == nil { + t.Fatal("should not have created git.log.yaml — tool exists in git.yaml") + } + + // Verify git.yaml still has both tools + data, err := os.ReadFile(multiFile) + if err != nil { + t.Fatal(err) + } + content := string(data) + if !strings.Contains(content, "git.log") { + t.Fatal("git.log should still be in git.yaml") + } + if !strings.Contains(content, "git.status") { + t.Fatal("git.status should still be in git.yaml") + } + if !strings.Contains(content, "--oneline") { + t.Fatal("git.log should have been updated with --oneline") + } +} + +func TestDeleteToolFromDir_MultiToolFile(t *testing.T) { + toolsDir := t.TempDir() + + // Write a multi-tool file + multiFile := filepath.Join(toolsDir, "git.yaml") + initial := []byte(`git.log: + type: cli + command: git +git.status: + type: cli + command: git +`) + if err := os.WriteFile(multiFile, initial, 0o644); err != nil { + t.Fatal(err) + } + + // Delete git.log — should remove from file, keep git.status + if err := deleteToolFromDir(toolsDir, "git.log"); err != nil { + t.Fatal(err) + } + + data, err := os.ReadFile(multiFile) + if err != nil { + t.Fatal(err) + } + content := string(data) + if strings.Contains(content, "git.log") { + t.Fatal("git.log should have been removed") + } + if !strings.Contains(content, "git.status") { + t.Fatal("git.status should still be present") + } +} + +// --- upsertConfigMapEntry / deleteConfigMapEntry tests --- + +func TestUpsertConfigMapEntry_NewEntry(t *testing.T) { + dir := t.TempDir() + cfgPath := filepath.Join(dir, "factorly.yaml") + _ = os.WriteFile(cfgPath, []byte("tools: {}\n"), 0o644) + + p := config.OAuthProviderConfig{ + ClientID: "abc", + ClientSecret: "secret", + AuthURL: "https://auth.example.com", + TokenURL: "https://token.example.com", + Scopes: []string{"read", "write"}, + } + + if err := upsertConfigMapEntry(cfgPath, "oauth_providers", "github", p); err != nil { + t.Fatal(err) + } + + data, _ := os.ReadFile(cfgPath) + content := string(data) + if !strings.Contains(content, "oauth_providers") { + t.Fatal("oauth_providers section not created") + } + if !strings.Contains(content, "github") { + t.Fatal("github provider not found") + } + if !strings.Contains(content, "abc") { + t.Fatal("client_id not written") + } + // Verify tools section still exists + if !strings.Contains(content, "tools") { + t.Fatal("tools section was lost") + } +} + +func TestUpsertConfigMapEntry_UpdateExisting(t *testing.T) { + dir := t.TempDir() + cfgPath := filepath.Join(dir, "factorly.yaml") + _ = os.WriteFile(cfgPath, []byte("oauth_providers:\n github:\n client_id: old\n client_secret: old_secret\n"), 0o644) + + p := config.OAuthProviderConfig{ + ClientID: "new_id", + ClientSecret: "new_secret", + AuthURL: "https://auth.example.com", + TokenURL: "https://token.example.com", + } + + if err := upsertConfigMapEntry(cfgPath, "oauth_providers", "github", p); err != nil { + t.Fatal(err) + } + + data, _ := os.ReadFile(cfgPath) + content := string(data) + if strings.Contains(content, "old") { + t.Fatal("old values should be replaced") + } + if !strings.Contains(content, "new_id") { + t.Fatal("new client_id not found") + } +} + +func TestDeleteConfigMapEntry(t *testing.T) { + dir := t.TempDir() + cfgPath := filepath.Join(dir, "factorly.yaml") + _ = os.WriteFile(cfgPath, []byte("oauth_providers:\n github:\n client_id: abc\n google:\n client_id: xyz\n"), 0o644) + + if err := deleteConfigMapEntry(cfgPath, "oauth_providers", "github"); err != nil { + t.Fatal(err) + } + + data, _ := os.ReadFile(cfgPath) + content := string(data) + if strings.Contains(content, "github") { + t.Fatal("github should be removed") + } + if !strings.Contains(content, "google") { + t.Fatal("google should still be present") + } +} + +func TestDeleteConfigMapEntry_LastEntry(t *testing.T) { + dir := t.TempDir() + cfgPath := filepath.Join(dir, "factorly.yaml") + _ = os.WriteFile(cfgPath, []byte("oauth_providers:\n github:\n client_id: abc\n"), 0o644) + + if err := deleteConfigMapEntry(cfgPath, "oauth_providers", "github"); err != nil { + t.Fatal(err) + } + + data, _ := os.ReadFile(cfgPath) + content := string(data) + if strings.Contains(content, "github") { + t.Fatal("github should be removed") + } + // oauth_providers key should still exist (empty mapping) + if !strings.Contains(content, "oauth_providers") { + t.Fatal("oauth_providers key should remain") + } +} + +func TestSaveOAuthProvider(t *testing.T) { + dir := t.TempDir() + cfgPath := filepath.Join(dir, "factorly.yaml") + _ = os.WriteFile(cfgPath, []byte("tools: {}\n"), 0o644) + + p := config.OAuthProviderConfig{ + ClientID: "id123", + ClientSecret: "{{vault:MY_SECRET}}", + AuthURL: "https://auth.example.com", + TokenURL: "https://token.example.com", + Scopes: []string{"read"}, + } + + if err := SaveOAuthProvider(cfgPath, "myapp", p); err != nil { + t.Fatal(err) + } + + data, _ := os.ReadFile(cfgPath) + content := string(data) + + // Verify field order: client_id should come first (struct field order) + clientIdx := strings.Index(content, "client_id") + authIdx := strings.Index(content, "auth_url") + if clientIdx == -1 || authIdx == -1 { + t.Fatal("expected both client_id and auth_url") + } + if clientIdx > authIdx { + t.Error("client_id should appear before auth_url (struct field order)") + } +} + +func TestDeleteOAuthProvider(t *testing.T) { + dir := t.TempDir() + cfgPath := filepath.Join(dir, "factorly.yaml") + _ = os.WriteFile(cfgPath, []byte("oauth_providers:\n github:\n client_id: x\n"), 0o644) + + if err := DeleteOAuthProvider(cfgPath, "github"); err != nil { + t.Fatal(err) + } + + data, _ := os.ReadFile(cfgPath) + if strings.Contains(string(data), "github") { + t.Fatal("github should be deleted") + } +} + +func TestSaveToolToConfig_PreservesOtherSections(t *testing.T) { + dir := t.TempDir() + cfgPath := filepath.Join(dir, "factorly.yaml") + _ = os.WriteFile(cfgPath, []byte("oauth_providers:\n github:\n client_id: abc\ntools:\n echo:\n type: cli\n command: echo\n"), 0o644) + + tc := config.ToolConfig{ + Type: "cli", + Command: "ls", + } + if err := saveToolToConfig(cfgPath, "ls.tool", tc); err != nil { + t.Fatal(err) + } + + data, _ := os.ReadFile(cfgPath) + content := string(data) + if !strings.Contains(content, "oauth_providers") { + t.Fatal("oauth_providers section was lost") + } + if !strings.Contains(content, "github") { + t.Fatal("github provider was lost") + } + if !strings.Contains(content, "ls.tool") { + t.Fatal("new tool not written") + } + if !strings.Contains(content, "echo") { + t.Fatal("existing tool was lost") + } +} + +func TestDeleteToolFromConfig_NonExistent(t *testing.T) { + dir := t.TempDir() + cfgPath := filepath.Join(dir, "factorly.yaml") + original := "tools:\n echo:\n type: cli\n command: echo\n" + _ = os.WriteFile(cfgPath, []byte(original), 0o644) + + // Deleting a non-existent tool should be a no-op + if err := deleteToolFromConfig(cfgPath, "nonexistent"); err != nil { + t.Fatal(err) + } + + data, _ := os.ReadFile(cfgPath) + if !strings.Contains(string(data), "echo") { + t.Fatal("existing tool should not be affected") + } +} + +func TestSaveToolToConfig_NoToolsSection(t *testing.T) { + dir := t.TempDir() + cfgPath := filepath.Join(dir, "factorly.yaml") + // Config with no tools section at all + _ = os.WriteFile(cfgPath, []byte("oauth_providers:\n github:\n client_id: x\n"), 0o644) + + tc := config.ToolConfig{ + Type: "cli", + Command: "echo", + } + if err := saveToolToConfig(cfgPath, "echo", tc); err != nil { + t.Fatal(err) + } + + data, _ := os.ReadFile(cfgPath) + content := string(data) + if !strings.Contains(content, "tools") { + t.Fatal("tools section should be created") + } + if !strings.Contains(content, "echo") { + t.Fatal("echo tool should be present") + } + if !strings.Contains(content, "oauth_providers") { + t.Fatal("other sections should be preserved") + } +} + +func TestRemoveToolFromFile_DeletesEmptyFile(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "single.yaml") + _ = os.WriteFile(path, []byte("only.tool:\n type: cli\n command: echo\n"), 0o644) + + if err := removeToolFromFile(path, "only.tool"); err != nil { + t.Fatal(err) + } + + // File should be deleted since it's now empty + if _, err := os.Stat(path); err == nil { + t.Fatal("file should be deleted when last tool is removed") + } +} + +func TestUpdateToolInFile_PreservesOrder(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "ordered.yaml") + _ = os.WriteFile(path, []byte("first:\n type: cli\n command: first\nsecond:\n type: cli\n command: second\nthird:\n type: cli\n command: third\n"), 0o644) + + tc := config.ToolConfig{ + Type: "cli", + Command: "updated_second", + } + if err := updateToolInFile(path, "second", tc); err != nil { + t.Fatal(err) + } + + data, _ := os.ReadFile(path) + content := string(data) + + // Verify order preserved: first, second, third + firstIdx := strings.Index(content, "first:") + secondIdx := strings.Index(content, "second:") + thirdIdx := strings.Index(content, "third:") + + if firstIdx > secondIdx || secondIdx > thirdIdx { + t.Errorf("order not preserved: first=%d second=%d third=%d", firstIdx, secondIdx, thirdIdx) + } + + if !strings.Contains(content, "updated_second") { + t.Error("second tool should be updated") + } +} + +func TestSafePath(t *testing.T) { + tests := []struct { + input string + ok bool + }{ + {"github.list_repos", true}, + {"my-tool", true}, + {"echo", true}, + {"", false}, + {".", false}, + {"..", false}, + {"../etc/passwd", false}, + {"foo/bar", false}, + {"foo\\bar", false}, + {"foo..bar", false}, + } + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + _, err := safePath(tt.input) + if tt.ok && err != nil { + t.Errorf("safePath(%q) should succeed, got error: %v", tt.input, err) + } + if !tt.ok && err == nil { + t.Errorf("safePath(%q) should fail, got nil error", tt.input) + } + }) + } +} + +func TestFindToolInDir(t *testing.T) { + dir := t.TempDir() + + // Single-tool file + _ = os.WriteFile(filepath.Join(dir, "echo.yaml"), []byte("echo:\n type: cli\n command: echo\n"), 0o644) + // Multi-tool file + _ = os.WriteFile(filepath.Join(dir, "git.yaml"), []byte("git.log:\n type: cli\n command: git\ngit.status:\n type: cli\n command: git\n"), 0o644) + + // Find in single-tool file + path := findToolInDir(dir, "echo") + if path == "" { + t.Fatal("should find echo") + } + if !strings.HasSuffix(path, "echo.yaml") { + t.Errorf("expected echo.yaml, got %s", path) + } + + // Find in multi-tool file + path = findToolInDir(dir, "git.log") + if path == "" { + t.Fatal("should find git.log") + } + if !strings.HasSuffix(path, "git.yaml") { + t.Errorf("expected git.yaml, got %s", path) + } + + // Not found + path = findToolInDir(dir, "nonexistent") + if path != "" { + t.Errorf("should not find nonexistent, got %s", path) + } +} + +func TestDeleteTool_RemovesFromLooseFiles(t *testing.T) { + // Simulate .factorly/ with a loose multi-tool file and a tools/ dir + dir := t.TempDir() + cfgPath := filepath.Join(dir, "factorly.yaml") + toolsDir := filepath.Join(dir, "tools") + _ = os.MkdirAll(toolsDir, 0o755) + + // Main config with empty tools + _ = os.WriteFile(cfgPath, []byte("tools: {}\n"), 0o644) + + // Loose file in config dir with multiple tools (like echo.yaml containing echo + env) + looseFile := filepath.Join(dir, "multi.yaml") + _ = os.WriteFile(looseFile, []byte("echo:\n type: cli\n command: echo\nenv:\n type: cli\n command: env\n"), 0o644) + + // Same tool also in tools dir + _ = os.WriteFile(filepath.Join(toolsDir, "env.yaml"), []byte("env:\n type: cli\n command: env\n"), 0o644) + + // Delete should remove from both locations + if err := DeleteTool(cfgPath, toolsDir, "env"); err != nil { + t.Fatal(err) + } + + // Verify removed from loose file + data, err := os.ReadFile(looseFile) + if err != nil { + t.Fatal(err) + } + content := string(data) + if strings.Contains(content, "env:") { + t.Error("env should be removed from loose file") + } + if !strings.Contains(content, "echo:") { + t.Error("echo should still be in loose file") + } + + // Verify removed from tools dir + if _, err := os.Stat(filepath.Join(toolsDir, "env.yaml")); err == nil { + t.Error("env.yaml should be deleted from tools dir") + } +} + +func TestSaveTool_CleansLooseFileDuplicates(t *testing.T) { + dir := t.TempDir() + cfgPath := filepath.Join(dir, "factorly.yaml") + toolsDir := filepath.Join(dir, "tools") + _ = os.MkdirAll(toolsDir, 0o755) + + _ = os.WriteFile(cfgPath, []byte("tools: {}\n"), 0o644) + + // Tool exists in a loose file + looseFile := filepath.Join(dir, "misc.yaml") + _ = os.WriteFile(looseFile, []byte("my.tool:\n type: cli\n command: old\n"), 0o644) + + // Save should remove from loose file and write to tools dir + tc := config.ToolConfig{ + Type: "cli", + Command: "new", + } + if err := SaveTool(cfgPath, toolsDir, "my.tool", tc); err != nil { + t.Fatal(err) + } + + // Verify removed from loose file (file should be deleted since it's now empty) + if _, err := os.Stat(looseFile); err == nil { + data, _ := os.ReadFile(looseFile) + if strings.Contains(string(data), "my.tool") { + t.Error("my.tool should be removed from loose file") + } + } + + // Verify saved in tools dir + saved := findToolInDir(toolsDir, "my.tool") + if saved == "" { + t.Error("my.tool should be in tools dir") + } +} + +func TestDeleteTool_LooseFileOnly(t *testing.T) { + // Tool only exists in a loose file, not in tools_dir or factorly.yaml + dir := t.TempDir() + cfgPath := filepath.Join(dir, "factorly.yaml") + toolsDir := filepath.Join(dir, "tools") + _ = os.MkdirAll(toolsDir, 0o755) + + _ = os.WriteFile(cfgPath, []byte("tools: {}\n"), 0o644) + _ = os.WriteFile(filepath.Join(dir, "standalone.yaml"), []byte("lonely:\n type: cli\n command: echo\n"), 0o644) + + if err := DeleteTool(cfgPath, toolsDir, "lonely"); err != nil { + t.Fatal(err) + } + + // File should be deleted (was the only tool) + if _, err := os.Stat(filepath.Join(dir, "standalone.yaml")); err == nil { + data, _ := os.ReadFile(filepath.Join(dir, "standalone.yaml")) + if strings.Contains(string(data), "lonely") { + t.Error("lonely should be removed") + } + } +} diff --git a/src/internal/vault/resolver.go b/src/internal/vault/resolver.go index 7b26535..0702898 100644 --- a/src/internal/vault/resolver.go +++ b/src/internal/vault/resolver.go @@ -9,21 +9,38 @@ import ( "strings" ) -var refPattern = regexp.MustCompile(`\{\{([A-Za-z0-9_][A-Za-z0-9_-]*):([A-Za-z0-9_./-]+)(?:\|([^}]*))?\}\}`) +// refPattern matches {{backend:content}} and {{backend:content|default}}. +// The content group uses .+? (non-greedy) to support both simple keys (MY_SECRET) +// and complex expressions (now('24h'), jsonpath(data, '$.field')). +var refPattern = regexp.MustCompile(`\{\{([A-Za-z0-9_][A-Za-z0-9_-]*):(.+?)(?:\|([^}]*))?\}\}`) -// Resolver resolves {{backend:key}} references by dispatching to registered backends. +// ResolveFunc is a lightweight read-only resolver for computed backends +// like expr that don't need Get/Set/Delete/List/Close. +type ResolveFunc func(content string) (string, error) + +// Resolver resolves {{backend:content}} references by dispatching to registered backends. type Resolver struct { backends map[string]Backend + funcs map[string]ResolveFunc } func NewResolver() *Resolver { - return &Resolver{backends: make(map[string]Backend)} + return &Resolver{ + backends: make(map[string]Backend), + funcs: make(map[string]ResolveFunc), + } } func (r *Resolver) Register(prefix string, b Backend) { r.backends[prefix] = b } +// RegisterFunc registers a lightweight resolver function for a prefix. +// Used for computed backends like "expr" that only resolve, never store. +func (r *Resolver) RegisterFunc(prefix string, fn ResolveFunc) { + r.funcs[prefix] = fn +} + // Backend returns a registered backend by name, or nil if not found. func (r *Resolver) Backend(name string) Backend { return r.backends[name] @@ -50,6 +67,19 @@ func (r *Resolver) Resolve(s string) (string, error) { if hasDefault { defaultVal = parts[3] } + // Try ResolveFunc first (lightweight computed backends like expr) + if fn, ok := r.funcs[backend]; ok { + val, err := fn(key) + if err != nil { + if hasDefault { + return defaultVal + } + return match + } + return val + } + + // Then try full Backend b, ok := r.backends[backend] if !ok { if hasDefault { @@ -91,6 +121,19 @@ func (r *Resolver) ResolveTracked(s string) (string, []string, error) { if hasDefault { defaultVal = parts[3] } + // Try ResolveFunc first + if fn, ok := r.funcs[backend]; ok { + val, err := fn(key) + if err != nil { + if hasDefault { + return defaultVal + } + return match + } + // Don't track expr funcs as accessed keys (they're not secrets) + return val + } + b, ok := r.backends[backend] if !ok { if hasDefault { @@ -113,6 +156,36 @@ func (r *Resolver) ResolveTracked(s string) (string, []string, error) { return result, accessed, resolveErr } +// Redact resolves vault refs in originalRefs and replaces the individual +// secret values (what the vault backend returned) with "****" in s. +// Used for safe verbose logging. +func (r *Resolver) Redact(s string, originalRefs []string) string { + if r == nil { + return s + } + for _, ref := range originalRefs { + // Extract each secret value from the ref + refPattern.ReplaceAllStringFunc(ref, func(match string) string { + parts := refPattern.FindStringSubmatch(match) + if len(parts) < 3 { + return match + } + backend, key := parts[1], parts[2] + b, ok := r.backends[backend] + if !ok { + return match + } + val, err := b.Get(key) + if err != nil || val == "" { + return match + } + s = strings.ReplaceAll(s, val, "****") + return match + }) + } + return s +} + // HasVaultRefs returns true if the string contains any {{backend:key}} references // that require vault access. Excludes {{env:VAR}} since env vars are resolved // at config load time — unresolved env refs mean the var isn't set, not that diff --git a/src/internal/vault/resolver_test.go b/src/internal/vault/resolver_test.go index bc3b933..cffa4b7 100644 --- a/src/internal/vault/resolver_test.go +++ b/src/internal/vault/resolver_test.go @@ -4,6 +4,7 @@ package vault import ( + "fmt" "strings" "testing" ) @@ -269,6 +270,74 @@ func TestHasVaultRefsWithDefault(t *testing.T) { } } +func TestRedact(t *testing.T) { + r := NewResolver() + r.Register("vault", &mockBackend{secrets: map[string]string{ + "TOKEN": "sk_live_secret_99999", + "DB_URL": "postgres://admin:pass@db:5432", + }}) + + tests := []struct { + name string + input string + refs []string + expected string + }{ + { + name: "redacts bearer token in header", + input: "Authorization: Bearer sk_live_secret_99999", + refs: []string{"{{vault:TOKEN}}"}, + expected: "Authorization: Bearer ****", + }, + { + name: "redacts inline vault ref in URL path", + input: "https://api.telegram.org/botsk_live_secret_99999/getMe", + refs: []string{"/bot{{vault:TOKEN}}/getMe"}, + expected: "https://api.telegram.org/bot****/getMe", + }, + { + name: "redacts multiple secrets", + input: "token=sk_live_secret_99999 db=postgres://admin:pass@db:5432", + refs: []string{"{{vault:TOKEN}}", "{{vault:DB_URL}}"}, + expected: "token=**** db=****", + }, + { + name: "no refs, no redaction", + input: "GET /public/endpoint", + refs: nil, + expected: "GET /public/endpoint", + }, + { + name: "unresolvable ref leaves string unchanged", + input: "key=plaintext", + refs: []string{"{{vault:NONEXISTENT}}"}, + expected: "key=plaintext", + }, + { + name: "ref that resolves to same value (no vault match) unchanged", + input: "value={{vault:MISSING}}", + refs: []string{"{{vault:MISSING}}"}, + expected: "value={{vault:MISSING}}", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := r.Redact(tt.input, tt.refs) + if result != tt.expected { + t.Errorf("Redact(%q) = %q, want %q", tt.input, result, tt.expected) + } + }) + } +} + +func TestRedact_NilResolver(t *testing.T) { + var r *Resolver + result := r.Redact("secret data", []string{"{{vault:KEY}}"}) + if result != "secret data" { + t.Errorf("nil resolver should return input unchanged, got %q", result) + } +} + // mapBackend is a simple in-memory backend for testing. type mapBackend struct { data map[string]string @@ -294,3 +363,111 @@ func (m *mapBackend) List() ([]string, error) { } func (m *mapBackend) Close() error { return nil } + +// --- ResolveFunc tests --- + +func TestResolveFuncExpr(t *testing.T) { + r := NewResolver() + r.RegisterFunc("expr", func(content string) (string, error) { + // Simulate a simple expression evaluator + if content == "now()" { + return "2026-05-09T00:00:00Z", nil + } + return content, nil + }) + + result, err := r.Resolve("time is {{expr:now()}}") + if err != nil { + t.Fatal(err) + } + if result != "time is 2026-05-09T00:00:00Z" { + t.Errorf("expected resolved expr, got %q", result) + } +} + +func TestResolveFuncWithVaultBackend(t *testing.T) { + r := NewResolver() + r.Register("vault", &mockBackend{secrets: map[string]string{"TOKEN": "secret123"}}) + r.RegisterFunc("expr", func(content string) (string, error) { + return "evaluated:" + content, nil + }) + + // Both should resolve in one pass + result, err := r.Resolve("key={{vault:TOKEN}} expr={{expr:hello}}") + if err != nil { + t.Fatal(err) + } + if result != "key=secret123 expr=evaluated:hello" { + t.Errorf("expected both resolved, got %q", result) + } +} + +func TestResolveFuncOverridesBackend(t *testing.T) { + r := NewResolver() + r.Register("test", &mockBackend{secrets: map[string]string{"key": "from_backend"}}) + r.RegisterFunc("test", func(content string) (string, error) { + return "from_func", nil + }) + + // Func takes priority over backend + result, err := r.Resolve("{{test:key}}") + if err != nil { + t.Fatal(err) + } + if result != "from_func" { + t.Errorf("expected func to take priority, got %q", result) + } +} + +func TestResolveFuncNotTracked(t *testing.T) { + r := NewResolver() + r.RegisterFunc("expr", func(content string) (string, error) { + return "resolved", nil + }) + + _, accessed, err := r.ResolveTracked("{{expr:something}}") + if err != nil { + t.Fatal(err) + } + if len(accessed) != 0 { + t.Errorf("expr funcs should not be tracked, got %v", accessed) + } +} + +func TestResolveFuncError(t *testing.T) { + r := NewResolver() + r.RegisterFunc("expr", func(content string) (string, error) { + return "", fmt.Errorf("eval error") + }) + + // With no default, should leave pattern unchanged + result, _ := r.Resolve("{{expr:bad}}") + if result != "{{expr:bad}}" { + t.Errorf("expected unchanged on error, got %q", result) + } +} + +func TestResolveFuncWithDefault(t *testing.T) { + r := NewResolver() + r.RegisterFunc("expr", func(content string) (string, error) { + return "", fmt.Errorf("eval error") + }) + + result, _ := r.Resolve("{{expr:bad|fallback}}") + if result != "fallback" { + t.Errorf("expected fallback on error, got %q", result) + } +} + +func TestResolveFuncComplexExpr(t *testing.T) { + r := NewResolver() + r.RegisterFunc("expr", func(content string) (string, error) { + // Verify the full expression content is passed through + return "got:" + content, nil + }) + + result, _ := r.Resolve("{{expr:jsonpath(data, '$.items[0].name')}}") + if result != "got:jsonpath(data, '$.items[0].name')" { + t.Errorf("expected full expression passed, got %q", result) + } +} diff --git a/src/test/integration_test.go b/src/test/integration_test.go index 18d9396..ebd0bc2 100644 --- a/src/test/integration_test.go +++ b/src/test/integration_test.go @@ -622,6 +622,7 @@ tools: base_url: %s method: POST path: /items + body_type: raw parameters: - name: body in: body