@fluid-app/fluid-cli-portal 0.1.41 → 0.1.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pull-Cl7omGLf.mjs","names":[],"sources":["../src/utils/atomic-write.ts","../src/utils/mappings.ts","../src/utils/snapshot.ts","../src/utils/transform.ts","../src/commands/pull.ts"],"sourcesContent":["/**\n * Atomic file write utility.\n *\n * Uses a write-then-rename pattern so readers always see either the old\n * file or the complete new file — never a partially-written state.\n */\n\nimport { randomBytes } from \"node:crypto\";\nimport { rename, unlink, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\n\n/**\n * Write a file atomically using a write-then-rename pattern.\n * Ensures readers always see either the old file or the complete new file.\n *\n * If `rename` fails after the temp file has been written, the temp file\n * is cleaned up on a best-effort basis before re-throwing the error.\n */\nexport async function atomicWriteFile(\n filePath: string,\n data: string,\n): Promise<void> {\n const dir = dirname(filePath);\n const tmp = join(dir, `.tmp-${randomBytes(6).toString(\"hex\")}`);\n try {\n await writeFile(tmp, data, \"utf-8\");\n await rename(tmp, filePath);\n } catch (err) {\n // Best-effort cleanup of the temp file\n await unlink(tmp).catch(() => {});\n throw err;\n }\n}\n","/**\n * Slug-to-ID mapping system for portal content sync.\n *\n * Maps human-readable slugs (derived from resource names) to API IDs,\n * enabling the file system to use readable directory/file names while\n * maintaining the link back to server-side resources.\n *\n * Persisted in `.portal-sync/mappings.json`.\n */\n\nimport { readFile, mkdir } from \"node:fs/promises\";\nimport { join, dirname } from \"node:path\";\n\nimport { atomicWriteFile } from \"./atomic-write.js\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Types\n// ─────────────────────────────────────────────────────────────────────────────\n\n/** Definition metadata stored at the top level of the mappings file. */\nexport interface DefinitionMapping {\n readonly name: string;\n readonly id: number;\n}\n\n/**\n * Resource types that support slug-to-ID mappings.\n * \"countries\" and \"ranks\" use display names as keys (not slugified).\n */\nexport type MappedResourceType =\n | \"screens\"\n | \"themes\"\n | \"navigations\"\n | \"profiles\"\n | \"countries\"\n | \"ranks\";\n\n/** Full mappings structure persisted to disk. */\nexport interface PortalMappings {\n readonly definition: DefinitionMapping;\n readonly screens: Record<string, number>;\n readonly themes: Record<string, number>;\n readonly navigations: Record<string, number>;\n readonly profiles: Record<string, number>;\n readonly countries: Record<string, number>;\n readonly ranks: Record<string, number>;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Helpers\n// ─────────────────────────────────────────────────────────────────────────────\n\n/** Runtime check that a parsed value has the expected PortalMappings shape. */\nfunction isMappings(value: unknown): value is PortalMappings {\n if (!value || typeof value !== \"object\") return false;\n const m = value as Record<string, unknown>;\n\n // Validate inner DefinitionMapping fields\n const def = m.definition as Record<string, unknown> | null | undefined;\n if (!def || typeof def !== \"object\") return false;\n if (typeof def.name !== \"string\" || typeof def.id !== \"number\") return false;\n\n return (\n typeof m.screens === \"object\" &&\n m.screens !== null &&\n typeof m.themes === \"object\" &&\n m.themes !== null &&\n typeof m.navigations === \"object\" &&\n m.navigations !== null &&\n typeof m.profiles === \"object\" &&\n m.profiles !== null &&\n typeof m.countries === \"object\" &&\n m.countries !== null &&\n typeof m.ranks === \"object\" &&\n m.ranks !== null\n );\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Read / Write\n// ─────────────────────────────────────────────────────────────────────────────\n\nconst MAPPINGS_FILE = \"mappings.json\";\n\n/**\n * Read the mappings file from the `.portal-sync/` directory.\n * Returns `null` if the file does not exist.\n */\nexport async function readMappings(\n portalSyncDir: string,\n): Promise<PortalMappings | null> {\n try {\n const filePath = join(portalSyncDir, MAPPINGS_FILE);\n const content = await readFile(filePath, \"utf-8\");\n const parsed: unknown = JSON.parse(content);\n if (!isMappings(parsed)) {\n throw new Error(`Malformed mappings file: ${filePath}`);\n }\n return parsed;\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n return null;\n }\n throw err;\n }\n}\n\n/**\n * Write the mappings file to the `.portal-sync/` directory.\n * Creates the directory if it does not exist.\n */\nexport async function writeMappings(\n portalSyncDir: string,\n mappings: PortalMappings,\n): Promise<void> {\n const filePath = join(portalSyncDir, MAPPINGS_FILE);\n await mkdir(dirname(filePath), { recursive: true });\n await atomicWriteFile(filePath, JSON.stringify(mappings, null, 2) + \"\\n\");\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Slug derivation\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Derive a filesystem-safe slug from a resource name.\n *\n * - Lowercases the name\n * - Replaces non-alphanumeric characters (except hyphens) with hyphens\n * - Collapses consecutive hyphens\n * - Trims leading/trailing hyphens\n *\n * If the derived slug collides with an existing slug in `existingSlugs`,\n * a numeric suffix is appended (e.g., `home-2`, `home-3`).\n */\nexport function deriveSlug(\n name: string,\n existingSlugs: ReadonlySet<string> = new Set(),\n): string {\n const base =\n name\n .toLowerCase()\n .replace(/[^a-z0-9-]/g, \"-\")\n .replace(/-+/g, \"-\")\n .replace(/^-|-$/g, \"\") || \"unnamed\";\n\n if (!existingSlugs.has(base)) {\n return base;\n }\n\n // Collision detected — append a numeric suffix\n let counter = 2;\n while (existingSlugs.has(`${base}-${counter}`)) {\n counter++;\n }\n return `${base}-${counter}`;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Lookup helpers\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Resolve a slug to its server-side ID.\n * Returns `undefined` if the slug is not mapped.\n */\nexport function resolveSlugToId(\n mappings: PortalMappings,\n resourceType: MappedResourceType,\n slug: string,\n): number | undefined {\n return mappings[resourceType][slug];\n}\n\n/**\n * Resolve a server-side ID back to its slug.\n * Returns `undefined` if no slug maps to the given ID.\n */\nexport function resolveIdToSlug(\n mappings: PortalMappings,\n resourceType: MappedResourceType,\n id: number,\n): string | undefined {\n const entries = mappings[resourceType];\n for (const [slug, mappedId] of Object.entries(entries)) {\n if (mappedId === id) {\n return slug;\n }\n }\n return undefined;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Mutation helpers (return new objects — no mutation)\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Add or update a mapping entry for a given resource type.\n * Returns a new `PortalMappings` object (immutable update).\n */\nexport function updateMapping(\n mappings: PortalMappings,\n resourceType: MappedResourceType,\n slug: string,\n id: number,\n): PortalMappings {\n return {\n ...mappings,\n [resourceType]: {\n ...mappings[resourceType],\n [slug]: id,\n },\n };\n}\n\n/**\n * Remove a mapping entry for a given resource type.\n * Returns a new `PortalMappings` object (immutable update).\n * If the slug does not exist, the original mappings are returned unchanged.\n */\nexport function removeMapping(\n mappings: PortalMappings,\n resourceType: MappedResourceType,\n slug: string,\n): PortalMappings {\n if (!(slug in mappings[resourceType])) {\n return mappings;\n }\n\n const { [slug]: _removed, ...rest } = mappings[resourceType];\n return {\n ...mappings,\n [resourceType]: rest,\n };\n}\n","/**\n * Hash-based snapshot system for portal content sync.\n *\n * Tracks file hashes to detect local changes since the last pull,\n * enabling efficient push operations that only send modified files.\n *\n * Persisted in `.portal-sync/snapshot.json`.\n */\n\nimport { createHash } from \"node:crypto\";\nimport { readFile, readdir, mkdir, stat } from \"node:fs/promises\";\nimport { join, dirname, relative, sep } from \"node:path\";\n\nimport { atomicWriteFile } from \"./atomic-write.js\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Types\n// ─────────────────────────────────────────────────────────────────────────────\n\n/** Maximum number of concurrent file operations. */\nconst MAX_CONCURRENCY = 20;\n\n/** SHA-256 hex digest of a file's contents. */\nexport type FileHash = string;\n\n/** Map of relative file paths to their content hashes. */\nexport type FileHashMap = Record<string, FileHash>;\n\n/** Snapshot structure persisted to `.portal-sync/snapshot.json`. */\nexport interface Snapshot {\n /** Human-readable name of the portal definition. */\n readonly definition: string;\n /** Server-side ID of the portal definition. */\n readonly definition_id: number;\n /** ISO 8601 timestamp of when the snapshot was created. */\n readonly pulled_at: string;\n /** Map of relative file paths → SHA-256 hashes. */\n readonly files: FileHashMap;\n}\n\n/** Result of diffing current files against a snapshot. */\nexport interface SnapshotDiff {\n /** Files that exist on disk but not in the snapshot. */\n readonly new: readonly string[];\n /** Files that exist in both but have different hashes. */\n readonly changed: readonly string[];\n /** Files that exist in the snapshot but not on disk. */\n readonly deleted: readonly string[];\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Helpers\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Run async tasks with bounded concurrency to avoid exhausting file descriptors.\n */\nasync function mapWithLimit<T, R>(\n items: readonly T[],\n limit: number,\n fn: (item: T) => Promise<R>,\n): Promise<R[]> {\n const results: R[] = new Array(items.length);\n let index = 0;\n\n async function worker(): Promise<void> {\n while (index < items.length) {\n const i = index++;\n results[i] = await fn(items[i]!);\n }\n }\n\n const workers = Array.from({ length: Math.min(limit, items.length) }, () =>\n worker(),\n );\n await Promise.all(workers);\n return results;\n}\n\n/** Runtime check that a parsed value has the expected Snapshot shape. */\nfunction isSnapshot(value: unknown): value is Snapshot {\n if (!value || typeof value !== \"object\") return false;\n const s = value as Record<string, unknown>;\n return (\n typeof s.definition === \"string\" &&\n typeof s.definition_id === \"number\" &&\n typeof s.pulled_at === \"string\" &&\n typeof s.files === \"object\" &&\n s.files !== null\n );\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Hashing\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Compute the SHA-256 hash of a file's contents.\n * Returns the hex-encoded digest.\n */\nexport async function computeFileHash(filePath: string): Promise<FileHash> {\n const content = await readFile(filePath);\n return createHash(\"sha256\").update(content).digest(\"hex\");\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Read / Write\n// ─────────────────────────────────────────────────────────────────────────────\n\nconst SNAPSHOT_FILE = \"snapshot.json\";\n\n/**\n * Read the snapshot file from the `.portal-sync/` directory.\n * Returns `null` if the file does not exist.\n */\nexport async function readSnapshot(\n portalSyncDir: string,\n): Promise<Snapshot | null> {\n try {\n const filePath = join(portalSyncDir, SNAPSHOT_FILE);\n const content = await readFile(filePath, \"utf-8\");\n const parsed: unknown = JSON.parse(content);\n if (!isSnapshot(parsed)) {\n throw new Error(`Malformed snapshot file: ${filePath}`);\n }\n return parsed;\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n return null;\n }\n throw err;\n }\n}\n\n/**\n * Write the snapshot file to the `.portal-sync/` directory.\n * Creates the directory if it does not exist.\n */\nexport async function writeSnapshot(\n portalSyncDir: string,\n snapshot: Snapshot,\n): Promise<void> {\n const filePath = join(portalSyncDir, SNAPSHOT_FILE);\n await mkdir(dirname(filePath), { recursive: true });\n await atomicWriteFile(filePath, JSON.stringify(snapshot, null, 2) + \"\\n\");\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Diff\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Recursively collect all files in a directory, returning paths\n * relative to `baseDir`.\n */\nasync function collectFiles(\n dir: string,\n baseDir: string = dir,\n): Promise<string[]> {\n const entries = await readdir(dir, { withFileTypes: true });\n const files: string[] = [];\n\n for (const entry of entries) {\n const fullPath = join(dir, entry.name);\n\n // Skip .portal-sync directory\n if (entry.name === \".portal-sync\") continue;\n\n if (entry.isSymbolicLink()) {\n // Resolve symlink to determine if it points to a file or directory\n const realStat = await stat(fullPath);\n if (realStat.isDirectory()) {\n files.push(...(await collectFiles(fullPath, baseDir)));\n } else if (realStat.isFile()) {\n files.push(relative(baseDir, fullPath).split(sep).join(\"/\"));\n }\n } else if (entry.isDirectory()) {\n files.push(...(await collectFiles(fullPath, baseDir)));\n } else {\n files.push(relative(baseDir, fullPath).split(sep).join(\"/\"));\n }\n }\n\n return files;\n}\n\n/**\n * Compare the current portal directory contents against a snapshot.\n *\n * Returns lists of new, changed, and deleted files (relative paths).\n */\nexport async function diffAgainstSnapshot(\n portalDir: string,\n snapshot: Snapshot,\n): Promise<SnapshotDiff> {\n const currentFiles = await collectFiles(portalDir);\n\n const snapshotPaths = new Set(Object.keys(snapshot.files));\n\n const newFiles: string[] = [];\n const changedFiles: string[] = [];\n\n // Check current files against snapshot (bounded concurrency)\n await mapWithLimit(currentFiles, MAX_CONCURRENCY, async (filePath) => {\n const hash = await computeFileHash(join(portalDir, filePath));\n\n if (!snapshotPaths.has(filePath)) {\n newFiles.push(filePath);\n } else if (snapshot.files[filePath] !== hash) {\n changedFiles.push(filePath);\n }\n });\n\n // Find deleted files (in snapshot but not on disk)\n const currentSet = new Set(currentFiles);\n const deletedFiles = [...snapshotPaths].filter(\n (filePath) => !currentSet.has(filePath),\n );\n\n return {\n new: [...newFiles].sort(),\n changed: [...changedFiles].sort(),\n deleted: [...deletedFiles].sort(),\n };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Build\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Build a fresh snapshot from the current portal directory contents.\n *\n * Hashes every file in `portalDir` (excluding `.portal-sync/`)\n * and records the definition metadata.\n */\nexport async function buildSnapshot(\n portalDir: string,\n definitionName: string,\n definitionId: number,\n): Promise<Snapshot> {\n const files = await collectFiles(portalDir);\n\n const hashEntries = await mapWithLimit(\n files,\n MAX_CONCURRENCY,\n async (filePath) => {\n const hash = await computeFileHash(join(portalDir, filePath));\n return [filePath, hash] as const;\n },\n );\n\n // Sort entries for deterministic output\n hashEntries.sort(([a], [b]) => a.localeCompare(b));\n\n const fileHashes: Record<string, FileHash> = {};\n for (const [path, hash] of hashEntries) {\n fileHashes[path] = hash;\n }\n\n return {\n definition: definitionName,\n definition_id: definitionId,\n pulled_at: new Date().toISOString(),\n files: fileHashes,\n };\n}\n","/**\n * Transformation utilities for converting Fluid OS API responses\n * into the local file format used by the portal content sync system.\n */\n\nimport type { components } from \"@fluid-app/fluidos-api-client\";\nimport { deriveSlug } from \"./mappings\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// API types (aliases for readability)\n// ─────────────────────────────────────────────────────────────────────────────\n\ntype ApiScreen = components[\"schemas\"][\"FluidOSScreen\"];\ntype ApiTheme = components[\"schemas\"][\"FluidOSTheme\"];\ntype ApiNavigationBasic = components[\"schemas\"][\"FluidOSNavigationBasic\"];\ntype ApiNavigationItem = components[\"schemas\"][\"FluidOSNavigationItem\"];\ntype ApiProfile = components[\"schemas\"][\"FluidOSProfile\"];\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Local file types\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface LocalScreen {\n readonly name: string;\n readonly component_tree: Record<string, unknown>[];\n}\n\nexport interface LocalTheme {\n readonly name: string;\n readonly config: Record<string, unknown>;\n readonly active: boolean;\n}\n\nexport interface LocalNavigationItem {\n readonly id: number;\n readonly icon: string | null;\n readonly label: string | null;\n readonly screen: string | null;\n readonly slug: string | null;\n readonly source: string;\n readonly position: number | null;\n readonly parent_id: number | null;\n readonly children: LocalNavigationItem[];\n}\n\nexport interface LocalNavigation {\n readonly name: string;\n readonly platform: string;\n readonly navigation_items: LocalNavigationItem[];\n}\n\nexport interface LocalProfile {\n readonly name: string;\n readonly default: boolean;\n readonly navigation: string | null;\n readonly mobile_navigation: string | null;\n readonly themes: string[];\n readonly permissions: {\n readonly ranks: number[];\n readonly roles: string[];\n readonly platform: string[];\n readonly countries: number[];\n };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Screen transformation\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Transform an API screen response into the local file format.\n * Strips server-only fields and normalizes `component_tree` to always be an array.\n */\nexport function transformScreen(screen: ApiScreen): LocalScreen {\n const name = screen.name ?? \"\";\n const rawTree = screen.component_tree;\n\n // Normalize component_tree to always be an array\n let componentTree: Record<string, unknown>[];\n if (rawTree == null) {\n componentTree = [];\n } else if (Array.isArray(rawTree)) {\n componentTree = rawTree as Record<string, unknown>[];\n } else {\n componentTree = [rawTree];\n }\n\n return { name, component_tree: componentTree };\n}\n\n/**\n * Derive a slug for a screen. Screens use the existing `slug` field from the API.\n * Falls back to slugifying the name if no slug exists.\n */\nexport function deriveScreenSlug(\n screen: ApiScreen,\n existingSlugs: Set<string>,\n): string {\n if (screen.slug) {\n // Sanitize: strip path-separator and other unsafe characters before using\n // the API slug as a filesystem path segment.\n const safe = screen.slug.replace(/[^a-z0-9_-]/gi, \"-\");\n if (!existingSlugs.has(safe)) {\n return safe;\n }\n return deriveSlug(safe, existingSlugs);\n }\n return deriveSlug(screen.name ?? \"unnamed\", existingSlugs);\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Theme transformation\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Transform an API theme response into the local file format.\n * Passes theme config through as-is (hex conversion is a separate ticket).\n */\nexport function transformTheme(theme: ApiTheme): LocalTheme {\n return {\n name: theme.name ?? \"\",\n config: theme.config ?? {},\n active: theme.active ?? false,\n };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Navigation transformation\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Build an ID → slug lookup map by inverting a slug → id mapping.\n */\nexport function buildIdToSlugMap(\n mappings: Record<string, number>,\n): Map<number, string> {\n const map = new Map<number, string>();\n for (const [slug, id] of Object.entries(mappings)) {\n map.set(id, slug);\n }\n return map;\n}\n\n/**\n * Transform API navigation items into local format.\n * Converts `screen_id` to `screen` slug reference. Keeps `id` for sync.\n */\nexport function transformNavigationItems(\n items: ApiNavigationItem[],\n screenIdToSlug: Map<number, string>,\n): LocalNavigationItem[] {\n return items.map((item) => ({\n id: item.id,\n icon: item.icon ?? null,\n label: item.label ?? null,\n screen: item.screen_id\n ? (screenIdToSlug.get(item.screen_id) ?? null)\n : null,\n slug: item.slug ?? null,\n source: item.source ?? \"user\",\n position: item.position ?? null,\n parent_id: item.parent_id ?? null,\n children: transformNavigationItems(item.children ?? [], screenIdToSlug),\n }));\n}\n\n/**\n * Transform an API navigation with its items into the local file format.\n */\nexport function transformNavigation(\n nav: ApiNavigationBasic,\n items: ApiNavigationItem[],\n screenIdToSlug: Map<number, string>,\n): LocalNavigation {\n return {\n name: nav.name ?? \"\",\n platform: nav.platform,\n navigation_items: transformNavigationItems(items, screenIdToSlug),\n };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Profile transformation\n// ─────────────────────────────────────────────────────────────────────────────\n\n/** Typed alias for call-site clarity. */\nexport const buildNavigationIdToSlugMap: (\n mappings: Record<string, number>,\n) => Map<number, string> = buildIdToSlugMap;\n\n/** Typed alias for call-site clarity. */\nexport const buildThemeIdToSlugMap: (\n mappings: Record<string, number>,\n) => Map<number, string> = buildIdToSlugMap;\n\n/**\n * Transform an API profile response into the local file format.\n * Converts navigation_id, mobile_navigation_id, and theme_ids to slug references.\n *\n * TODO (REP-842): Country/rank IDs in profile permissions need to be translated\n * to human-readable names via the main Fluid API. For now, the permission arrays\n * are written as-is with integer IDs.\n */\nexport function transformProfile(\n profile: ApiProfile,\n navIdToSlug: Map<number, string>,\n themeIdToSlug: Map<number, string>,\n): LocalProfile {\n const navigation = profile.navigation\n ? (navIdToSlug.get(profile.navigation.id) ?? null)\n : null;\n\n const mobileNavigation = profile.mobile_navigation\n ? (navIdToSlug.get(profile.mobile_navigation.id) ?? null)\n : null;\n\n const themes = (profile.themes ?? [])\n .map((t) => themeIdToSlug.get(t.id))\n .filter((slug): slug is string => slug != null);\n\n const permissions = profile.permissions ?? {};\n\n return {\n name: profile.name ?? \"\",\n default: profile.default ?? false,\n navigation,\n mobile_navigation: mobileNavigation,\n themes,\n permissions: {\n ranks: permissions.ranks ?? [],\n roles: permissions.roles ?? [],\n platform: permissions.platform ?? [],\n countries: permissions.countries ?? [],\n },\n };\n}\n","/**\n * `fluid portal pull` command\n *\n * Pulls a Fluid OS definition's resources (screens, themes, navigations,\n * profiles) from the API and writes them to the local `portal/` directory\n * as JSON files, along with `.portal-sync/` metadata for push diffing.\n */\n\nimport { Command } from \"commander\";\nimport { getAuthToken, getActiveProfile } from \"@fluid-app/fluid-cli\";\nimport { createFetchClient } from \"@fluid-app/fluidos-api-client\";\nimport type { FetchClient, components } from \"@fluid-app/fluidos-api-client\";\nimport { fluidOs } from \"@fluid-app/fluidos-api-client\";\nimport chalk from \"chalk\";\nimport ora from \"ora\";\nimport pLimit from \"p-limit\";\nimport { join } from \"node:path\";\nimport { mkdir, writeFile, rm, rename } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport prompts from \"prompts\";\n\nimport { deriveSlug, writeMappings } from \"../utils/mappings.js\";\nimport type { PortalMappings } from \"../utils/mappings.js\";\nimport {\n readSnapshot,\n diffAgainstSnapshot,\n buildSnapshot,\n writeSnapshot,\n} from \"../utils/snapshot.js\";\nimport {\n transformScreen,\n deriveScreenSlug,\n transformTheme,\n transformNavigation,\n transformProfile,\n buildIdToSlugMap,\n buildNavigationIdToSlugMap,\n buildThemeIdToSlugMap,\n} from \"../utils/transform.js\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Types\n// ─────────────────────────────────────────────────────────────────────────────\n\ntype ApiDefinitionBasic = components[\"schemas\"][\"FluidOSDefinitionBasic\"];\n\ninterface PullOptions {\n app?: string;\n force?: boolean;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Constants\n// ─────────────────────────────────────────────────────────────────────────────\n\nconst PORTAL_DIR = \"portal\";\nconst PORTAL_SYNC_DIR = \".portal-sync\";\nconst PAGE_LIMIT = 500;\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Helpers\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Create an authenticated FetchClient using the stored CLI profile.\n */\nfunction createClient(): FetchClient {\n const token = getAuthToken();\n if (!token) {\n const profile = getActiveProfile();\n if (!profile) {\n throw new Error(\n \"Not logged in. Run \" + chalk.cyan(\"fluid login\") + \" first.\",\n );\n }\n throw new Error(\n \"No auth token found for profile \" +\n chalk.cyan(profile.name) +\n \". Run \" +\n chalk.cyan(\"fluid login\") +\n \" to re-authenticate.\",\n );\n }\n\n const baseUrl = process.env[\"FLUID_API_BASE\"] ?? \"https://api.fluid.app\";\n\n return createFetchClient({\n baseUrl,\n getAuthToken: () => token,\n });\n}\n\n/**\n * Select a definition interactively or by name.\n */\nasync function fetchAllDefinitions(\n client: FetchClient,\n): Promise<ApiDefinitionBasic[]> {\n const all: ApiDefinitionBasic[] = [];\n let page = 1;\n\n while (true) {\n const response = await fluidOs.fluid_os_v0_list_fluid_osdefinitions(\n client,\n {\n page,\n per_page: PAGE_LIMIT,\n },\n );\n const batch = response.definitions ?? [];\n all.push(...batch);\n\n const totalPages = response.meta?.total_pages ?? 1;\n if (page >= totalPages || batch.length === 0) break;\n page++;\n }\n\n return all;\n}\n\nasync function selectDefinition(\n client: FetchClient,\n appName?: string,\n): Promise<ApiDefinitionBasic> {\n const definitions = await fetchAllDefinitions(client);\n\n if (definitions.length === 0) {\n throw new Error(\n \"No Fluid OS definitions found. Create one in the admin dashboard first.\",\n );\n }\n\n // If --app flag provided, find by name\n if (appName) {\n const match = definitions.find(\n (d) => d.name?.toLowerCase() === appName.toLowerCase(),\n );\n if (!match) {\n const availableNames = definitions\n .map((d) => chalk.cyan(d.name ?? \"(unnamed)\"))\n .join(\", \");\n throw new Error(\n `No definition found matching \"${appName}\". Available: ${availableNames}`,\n );\n }\n return match;\n }\n\n // Interactive selector\n const { definitionId } = await prompts({\n type: \"select\",\n name: \"definitionId\",\n message: \"Select a Fluid OS definition to pull\",\n choices: definitions.map((d) => ({\n title: d.name ?? \"(unnamed)\",\n value: d.id,\n })),\n });\n\n if (definitionId == null) {\n throw new Error(\"No definition selected.\");\n }\n\n const selected = definitions.find((d) => d.id === definitionId);\n if (!selected) {\n throw new Error(\"Selected definition not found.\");\n }\n\n return selected;\n}\n\n/**\n * Check for definition switching conflicts.\n * Returns true if it's safe to proceed.\n */\nasync function checkDefinitionSwitch(\n cwd: string,\n newDefinitionId: number,\n newDefinitionName: string,\n force: boolean,\n): Promise<boolean> {\n const portalDir = join(cwd, PORTAL_DIR);\n const portalSyncDir = join(cwd, PORTAL_SYNC_DIR);\n\n // No existing portal directory — always safe\n if (!existsSync(portalDir)) {\n return true;\n }\n\n // Read existing snapshot to see what definition was pulled\n const snapshot = await readSnapshot(portalSyncDir);\n if (!snapshot) {\n // No snapshot means no prior pull — safe to overwrite\n return true;\n }\n\n // Same definition — this is a refresh\n if (snapshot.definition_id === newDefinitionId) {\n return true;\n }\n\n // Different definition — check for local changes\n const diff = await diffAgainstSnapshot(portalDir, snapshot);\n const hasChanges =\n diff.new.length > 0 || diff.changed.length > 0 || diff.deleted.length > 0;\n\n if (hasChanges && !force) {\n console.log();\n console.log(\n chalk.yellow(\"Warning:\") +\n \" Switching from \" +\n chalk.cyan(snapshot.definition) +\n \" to \" +\n chalk.cyan(newDefinitionName) +\n \" with unpushed local changes.\",\n );\n console.log();\n console.log(\" Modified: \" + diff.changed.length + \" file(s)\");\n console.log(\" New: \" + diff.new.length + \" file(s)\");\n console.log(\" Deleted: \" + diff.deleted.length + \" file(s)\");\n console.log();\n console.log(\n \"Use \" +\n chalk.cyan(\"--force\") +\n \" to discard local changes and switch definitions.\",\n );\n return false;\n }\n\n return true;\n}\n\n/**\n * Write a JSON file to the portal directory, creating subdirectories as needed.\n */\nasync function writePortalFile(\n portalDir: string,\n relativePath: string,\n data: unknown,\n): Promise<void> {\n const filePath = join(portalDir, relativePath);\n const dir = join(filePath, \"..\");\n await mkdir(dir, { recursive: true });\n await writeFile(filePath, JSON.stringify(data, null, 2) + \"\\n\", \"utf-8\");\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Command\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport const pullCommand: Command = new Command(\"pull\")\n .description(\n \"Pull a Fluid OS definition's resources to the local portal/ directory\",\n )\n .option(\"--app <name>\", \"Definition name (skips interactive selector)\")\n .option(\"--force\", \"Overwrite local changes when switching definitions\")\n .action(async (options: PullOptions) => {\n const cwd = process.cwd();\n\n console.log();\n console.log(chalk.blue.bold(\"Fluid Portal Pull\"));\n console.log();\n\n // ── Authenticate ──────────────────────────────────────────────────\n const spinner = ora();\n spinner.start(\"Authenticating...\");\n\n let client: FetchClient;\n try {\n client = createClient();\n spinner.succeed(\"Authenticated\");\n } catch (err) {\n spinner.fail(\"Authentication failed\");\n console.log();\n console.log(\n chalk.red(\"Error:\") +\n \" \" +\n (err instanceof Error ? err.message : String(err)),\n );\n console.log();\n process.exit(1);\n }\n\n // ── Select definition ─────────────────────────────────────────────\n let definition: ApiDefinitionBasic;\n try {\n definition = await selectDefinition(client, options.app);\n } catch (err) {\n console.log();\n console.log(\n chalk.red(\"Error:\") +\n \" \" +\n (err instanceof Error ? err.message : String(err)),\n );\n console.log();\n process.exit(1);\n }\n\n const definitionId = definition.id;\n const definitionName = definition.name ?? \"(unnamed)\";\n\n console.log(\n chalk.gray(\"Definition: \") +\n chalk.white(definitionName) +\n chalk.gray(` (ID: ${definitionId})`),\n );\n console.log();\n\n // ── Check definition switch ───────────────────────────────────────\n let canProceed: boolean;\n try {\n canProceed = await checkDefinitionSwitch(\n cwd,\n definitionId,\n definitionName,\n options.force ?? false,\n );\n } catch (err) {\n console.log();\n console.log(\n chalk.red(\"Error:\") +\n \" \" +\n (err instanceof Error ? err.message : String(err)),\n );\n console.log();\n process.exit(1);\n }\n if (!canProceed) {\n process.exit(1);\n }\n\n // ── Fetch all resources in parallel ───────────────────────────────\n spinner.start(\"Fetching resources...\");\n\n let screenBasics: NonNullable<\n Awaited<\n ReturnType<typeof fluidOs.fluid_os_v0_list_fluid_osscreens>\n >[\"screens\"]\n >;\n let themes: NonNullable<\n Awaited<\n ReturnType<typeof fluidOs.fluid_os_v0_list_fluid_osthemes>\n >[\"themes\"]\n >;\n let navigations: NonNullable<\n Awaited<\n ReturnType<typeof fluidOs.fluid_os_v0_list_fluid_osnavigations>\n >[\"navigations\"]\n >;\n let profiles: NonNullable<\n Awaited<\n ReturnType<typeof fluidOs.fluid_os_v0_list_fluid_osprofiles>\n >[\"profiles\"]\n >;\n let screens: NonNullable<\n Awaited<\n ReturnType<typeof fluidOs.fluid_os_v0_get_fluid_osscreen>\n >[\"screen\"]\n >[];\n const navigationItemsMap = new Map<\n number,\n components[\"schemas\"][\"FluidOSNavigationItem\"][]\n >();\n\n try {\n const [\n screensResponse,\n themesResponse,\n navigationsResponse,\n profilesResponse,\n ] = await Promise.all([\n fluidOs.fluid_os_v0_list_fluid_osscreens(client, definitionId, {\n per_page: PAGE_LIMIT,\n }),\n fluidOs.fluid_os_v0_list_fluid_osthemes(client, definitionId, {\n per_page: PAGE_LIMIT,\n }),\n fluidOs.fluid_os_v0_list_fluid_osnavigations(client, definitionId, {\n per_page: PAGE_LIMIT,\n }),\n fluidOs.fluid_os_v0_list_fluid_osprofiles(client, definitionId, {\n per_page: PAGE_LIMIT,\n }),\n ]);\n\n screenBasics = screensResponse.screens ?? [];\n themes = themesResponse.themes ?? [];\n navigations = navigationsResponse.navigations ?? [];\n profiles = profilesResponse.profiles ?? [];\n\n // Warn if any list hits the page limit\n if (screenBasics.length === PAGE_LIMIT) {\n console.warn(\n chalk.yellow(\n `Warning: screen count hit the ${PAGE_LIMIT}-item limit; some screens may be missing.`,\n ),\n );\n }\n if (themes.length === PAGE_LIMIT) {\n console.warn(\n chalk.yellow(\n `Warning: theme count hit the ${PAGE_LIMIT}-item limit; some themes may be missing.`,\n ),\n );\n }\n if (navigations.length === PAGE_LIMIT) {\n console.warn(\n chalk.yellow(\n `Warning: navigation count hit the ${PAGE_LIMIT}-item limit; some navigations may be missing.`,\n ),\n );\n }\n if (profiles.length === PAGE_LIMIT) {\n console.warn(\n chalk.yellow(\n `Warning: profile count hit the ${PAGE_LIMIT}-item limit; some profiles may be missing.`,\n ),\n );\n }\n\n spinner.text = \"Fetching screen details...\";\n\n // Limit concurrency to avoid hitting API rate limits\n const limit = pLimit(10);\n\n // Fetch full screen details (list endpoint doesn't include component_tree)\n screens = await Promise.all(\n screenBasics.map((s) =>\n limit(async () => {\n const res = await fluidOs.fluid_os_v0_get_fluid_osscreen(\n client,\n definitionId,\n s.id,\n );\n if (!res.screen) {\n throw new Error(`Failed to fetch details for screen ID ${s.id}`);\n }\n return res.screen;\n }),\n ),\n );\n\n // Fetch navigation items for each navigation.\n // NOTE: The fluid_os_v0_list_fluid_osnavigation_items API endpoint does not support\n // pagination query parameters (per_page/page). Navigation items are\n // returned as a nested tree structure and are not paginated.\n spinner.text = \"Fetching navigation items...\";\n await Promise.all(\n navigations.map((nav) =>\n limit(async () => {\n const res = await fluidOs.fluid_os_v0_list_fluid_osnavigation_items(\n client,\n definitionId,\n nav.id,\n );\n navigationItemsMap.set(nav.id, res.navigation_items ?? []);\n }),\n ),\n );\n\n spinner.succeed(\n `Fetched ${screens.length} screen(s), ${themes.length} theme(s), ${navigations.length} navigation(s), ${profiles.length} profile(s)`,\n );\n } catch (err) {\n spinner.fail(\"Failed to fetch resources\");\n console.log();\n console.log(\n chalk.red(\"Error:\") +\n \" \" +\n (err instanceof Error ? err.message : String(err)),\n );\n console.log();\n process.exit(1);\n }\n\n // ── Build slug mappings ───────────────────────────────────────────\n spinner.start(\"Transforming resources...\");\n\n // Screen mappings: use existing slug field when available\n const screenSlugs = new Set<string>();\n const screenMappings: Record<string, number> = {};\n for (const screen of screens) {\n const slug = deriveScreenSlug(screen, screenSlugs);\n screenSlugs.add(slug);\n screenMappings[slug] = screen.id;\n }\n\n // Theme mappings: slugify from name\n const themeSlugs = new Set<string>();\n const themeMappings: Record<string, number> = {};\n for (const theme of themes) {\n const slug = deriveSlug(theme.name ?? \"unnamed\", themeSlugs);\n themeSlugs.add(slug);\n themeMappings[slug] = theme.id;\n }\n\n // Navigation mappings: slugify from name\n const navSlugs = new Set<string>();\n const navMappings: Record<string, number> = {};\n for (const nav of navigations) {\n const slug = deriveSlug(nav.name ?? \"unnamed\", navSlugs);\n navSlugs.add(slug);\n navMappings[slug] = nav.id;\n }\n\n // Profile mappings: slugify from name\n const profileSlugs = new Set<string>();\n const profileMappings: Record<string, number> = {};\n for (const profile of profiles) {\n const slug = deriveSlug(profile.name ?? \"unnamed\", profileSlugs);\n profileSlugs.add(slug);\n profileMappings[slug] = profile.id;\n }\n\n // Build reverse-lookup maps for ID → slug references\n const screenIdToSlug = buildIdToSlugMap(screenMappings);\n const navIdToSlug = buildNavigationIdToSlugMap(navMappings);\n const themeIdToSlug = buildThemeIdToSlugMap(themeMappings);\n const profileIdToSlug = buildIdToSlugMap(profileMappings);\n\n spinner.succeed(\"Resources transformed\");\n\n // ── Clean and write files ─────────────────────────────────────────\n spinner.start(\"Writing files...\");\n\n const portalDir = join(cwd, PORTAL_DIR);\n const portalSyncDir = join(cwd, PORTAL_SYNC_DIR);\n\n // Write to a temporary directory first, then atomically swap into place.\n // This ensures the existing portal/ directory is only removed once all\n // new content has been successfully written.\n const tmpPortalDir = portalDir + \".tmp\";\n\n try {\n // Clean up any leftover temp directory from a previous failed run\n if (existsSync(tmpPortalDir)) {\n await rm(tmpPortalDir, { recursive: true });\n }\n await mkdir(tmpPortalDir, { recursive: true });\n\n // Write definition.json\n await writePortalFile(tmpPortalDir, \"definition.json\", {\n name: definitionName,\n });\n\n // Write screens\n for (const screen of screens) {\n const slug = screenIdToSlug.get(screen.id)!;\n const local = transformScreen(screen);\n await writePortalFile(tmpPortalDir, `screens/${slug}.json`, local);\n }\n\n // Write themes\n for (const theme of themes) {\n const slug = themeIdToSlug.get(theme.id)!;\n const local = transformTheme(theme);\n await writePortalFile(tmpPortalDir, `themes/${slug}.json`, local);\n }\n\n // Write navigations\n for (const nav of navigations) {\n const slug = navIdToSlug.get(nav.id)!;\n const items = navigationItemsMap.get(nav.id) ?? [];\n const local = transformNavigation(nav, items, screenIdToSlug);\n await writePortalFile(tmpPortalDir, `navigations/${slug}.json`, local);\n }\n\n // Write profiles\n for (const profile of profiles) {\n const slug = profileIdToSlug.get(profile.id);\n if (slug) {\n const local = transformProfile(profile, navIdToSlug, themeIdToSlug);\n await writePortalFile(tmpPortalDir, `profiles/${slug}.json`, local);\n }\n }\n\n // Atomic swap: remove old directory, rename temp into place\n if (existsSync(portalDir)) {\n await rm(portalDir, { recursive: true });\n }\n await rename(tmpPortalDir, portalDir);\n } catch (err) {\n // Clean up temp directory on failure\n if (existsSync(tmpPortalDir)) {\n await rm(tmpPortalDir, { recursive: true }).catch(() => {});\n }\n spinner.fail(\"Failed to write files\");\n console.log();\n console.log(\n chalk.red(\"Error:\") +\n \" \" +\n (err instanceof Error ? err.message : String(err)),\n );\n console.log();\n process.exit(1);\n }\n\n // ── Write metadata (mappings + snapshot) ────────────────────────────\n // Separated from the file-write block so that a metadata failure does\n // not incorrectly report total failure when portal files are already\n // on disk after the atomic swap.\n try {\n const mappings: PortalMappings = {\n definition: { name: definitionName, id: definitionId },\n screens: screenMappings,\n themes: themeMappings,\n navigations: navMappings,\n profiles: profileMappings,\n countries: {},\n ranks: {},\n };\n await writeMappings(portalSyncDir, mappings);\n\n const snapshot = await buildSnapshot(\n portalDir,\n definitionName,\n definitionId,\n );\n await writeSnapshot(portalSyncDir, snapshot);\n\n spinner.succeed(\"Files written\");\n } catch (err) {\n spinner.warn(\"Files written (metadata sync failed)\");\n console.log();\n console.log(\n chalk.yellow(\"Warning:\") +\n \" Portal files were written successfully, but metadata sync failed.\" +\n \" Run \" +\n chalk.cyan(\"fluid portal pull\") +\n \" again to regenerate metadata.\",\n );\n console.log(\n chalk.gray(\" Detail: \") +\n (err instanceof Error ? err.message : String(err)),\n );\n console.log();\n }\n\n // ── Summary ───────────────────────────────────────────────────────\n console.log();\n console.log(chalk.green.bold(\"Pull complete!\"));\n console.log();\n console.log(chalk.gray(\" portal/definition.json\"));\n console.log(\n chalk.gray(` portal/screens/ `) +\n chalk.white(`${screens.length} file(s)`),\n );\n console.log(\n chalk.gray(` portal/themes/ `) +\n chalk.white(`${themes.length} file(s)`),\n );\n console.log(\n chalk.gray(` portal/navigations/ `) +\n chalk.white(`${navigations.length} file(s)`),\n );\n console.log(\n chalk.gray(` portal/profiles/ `) +\n chalk.white(`${profiles.length} file(s)`),\n );\n console.log(\n chalk.gray(` .portal-sync/ `) +\n chalk.white(\"mappings.json + snapshot.json\"),\n );\n console.log();\n });\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkBA,eAAsB,gBACpB,UACA,MACe;CAEf,MAAM,MAAM,KADA,QAAQ,SAAS,EACP,QAAQ,YAAY,EAAE,CAAC,SAAS,MAAM,GAAG;AAC/D,KAAI;AACF,QAAM,UAAU,KAAK,MAAM,QAAQ;AACnC,QAAM,OAAO,KAAK,SAAS;UACpB,KAAK;AAEZ,QAAM,OAAO,IAAI,CAAC,YAAY,GAAG;AACjC,QAAM;;;;;;;;;;;;;;;ACuBV,SAAS,WAAW,OAAyC;AAC3D,KAAI,CAAC,SAAS,OAAO,UAAU,SAAU,QAAO;CAChD,MAAM,IAAI;CAGV,MAAM,MAAM,EAAE;AACd,KAAI,CAAC,OAAO,OAAO,QAAQ,SAAU,QAAO;AAC5C,KAAI,OAAO,IAAI,SAAS,YAAY,OAAO,IAAI,OAAO,SAAU,QAAO;AAEvE,QACE,OAAO,EAAE,YAAY,YACrB,EAAE,YAAY,QACd,OAAO,EAAE,WAAW,YACpB,EAAE,WAAW,QACb,OAAO,EAAE,gBAAgB,YACzB,EAAE,gBAAgB,QAClB,OAAO,EAAE,aAAa,YACtB,EAAE,aAAa,QACf,OAAO,EAAE,cAAc,YACvB,EAAE,cAAc,QAChB,OAAO,EAAE,UAAU,YACnB,EAAE,UAAU;;AAQhB,MAAM,gBAAgB;;;;;AAMtB,eAAsB,aACpB,eACgC;AAChC,KAAI;EACF,MAAM,WAAW,KAAK,eAAe,cAAc;EACnD,MAAM,UAAU,MAAM,SAAS,UAAU,QAAQ;EACjD,MAAM,SAAkB,KAAK,MAAM,QAAQ;AAC3C,MAAI,CAAC,WAAW,OAAO,CACrB,OAAM,IAAI,MAAM,4BAA4B,WAAW;AAEzD,SAAO;UACA,KAAK;AACZ,MAAK,IAA8B,SAAS,SAC1C,QAAO;AAET,QAAM;;;;;;;AAQV,eAAsB,cACpB,eACA,UACe;CACf,MAAM,WAAW,KAAK,eAAe,cAAc;AACnD,OAAM,MAAM,QAAQ,SAAS,EAAE,EAAE,WAAW,MAAM,CAAC;AACnD,OAAM,gBAAgB,UAAU,KAAK,UAAU,UAAU,MAAM,EAAE,GAAG,KAAK;;;;;;;;;;;;;AAkB3E,SAAgB,WACd,MACA,gCAAqC,IAAI,KAAK,EACtC;CACR,MAAM,OACJ,KACG,aAAa,CACb,QAAQ,eAAe,IAAI,CAC3B,QAAQ,OAAO,IAAI,CACnB,QAAQ,UAAU,GAAG,IAAI;AAE9B,KAAI,CAAC,cAAc,IAAI,KAAK,CAC1B,QAAO;CAIT,IAAI,UAAU;AACd,QAAO,cAAc,IAAI,GAAG,KAAK,GAAG,UAAU,CAC5C;AAEF,QAAO,GAAG,KAAK,GAAG;;;;;;AAWpB,SAAgB,gBACd,UACA,cACA,MACoB;AACpB,QAAO,SAAS,cAAc;;;;;;AAOhC,SAAgB,gBACd,UACA,cACA,IACoB;CACpB,MAAM,UAAU,SAAS;AACzB,MAAK,MAAM,CAAC,MAAM,aAAa,OAAO,QAAQ,QAAQ,CACpD,KAAI,aAAa,GACf,QAAO;;;;;;AAcb,SAAgB,cACd,UACA,cACA,MACA,IACgB;AAChB,QAAO;EACL,GAAG;GACF,eAAe;GACd,GAAG,SAAS;IACX,OAAO;GACT;EACF;;;;;;;AAQH,SAAgB,cACd,UACA,cACA,MACgB;AAChB,KAAI,EAAE,QAAQ,SAAS,eACrB,QAAO;CAGT,MAAM,GAAG,OAAO,UAAU,GAAG,SAAS,SAAS;AAC/C,QAAO;EACL,GAAG;GACF,eAAe;EACjB;;;;;;;;;;;;;ACrNH,MAAM,kBAAkB;;;;AAqCxB,eAAe,aACb,OACA,OACA,IACc;CACd,MAAM,UAAe,IAAI,MAAM,MAAM,OAAO;CAC5C,IAAI,QAAQ;CAEZ,eAAe,SAAwB;AACrC,SAAO,QAAQ,MAAM,QAAQ;GAC3B,MAAM,IAAI;AACV,WAAQ,KAAK,MAAM,GAAG,MAAM,GAAI;;;CAIpC,MAAM,UAAU,MAAM,KAAK,EAAE,QAAQ,KAAK,IAAI,OAAO,MAAM,OAAO,EAAE,QAClE,QAAQ,CACT;AACD,OAAM,QAAQ,IAAI,QAAQ;AAC1B,QAAO;;;AAIT,SAAS,WAAW,OAAmC;AACrD,KAAI,CAAC,SAAS,OAAO,UAAU,SAAU,QAAO;CAChD,MAAM,IAAI;AACV,QACE,OAAO,EAAE,eAAe,YACxB,OAAO,EAAE,kBAAkB,YAC3B,OAAO,EAAE,cAAc,YACvB,OAAO,EAAE,UAAU,YACnB,EAAE,UAAU;;;;;;AAYhB,eAAsB,gBAAgB,UAAqC;CACzE,MAAM,UAAU,MAAM,SAAS,SAAS;AACxC,QAAO,WAAW,SAAS,CAAC,OAAO,QAAQ,CAAC,OAAO,MAAM;;AAO3D,MAAM,gBAAgB;;;;;AAMtB,eAAsB,aACpB,eAC0B;AAC1B,KAAI;EACF,MAAM,WAAW,KAAK,eAAe,cAAc;EACnD,MAAM,UAAU,MAAM,SAAS,UAAU,QAAQ;EACjD,MAAM,SAAkB,KAAK,MAAM,QAAQ;AAC3C,MAAI,CAAC,WAAW,OAAO,CACrB,OAAM,IAAI,MAAM,4BAA4B,WAAW;AAEzD,SAAO;UACA,KAAK;AACZ,MAAK,IAA8B,SAAS,SAC1C,QAAO;AAET,QAAM;;;;;;;AAQV,eAAsB,cACpB,eACA,UACe;CACf,MAAM,WAAW,KAAK,eAAe,cAAc;AACnD,OAAM,MAAM,QAAQ,SAAS,EAAE,EAAE,WAAW,MAAM,CAAC;AACnD,OAAM,gBAAgB,UAAU,KAAK,UAAU,UAAU,MAAM,EAAE,GAAG,KAAK;;;;;;AAW3E,eAAe,aACb,KACA,UAAkB,KACC;CACnB,MAAM,UAAU,MAAM,QAAQ,KAAK,EAAE,eAAe,MAAM,CAAC;CAC3D,MAAM,QAAkB,EAAE;AAE1B,MAAK,MAAM,SAAS,SAAS;EAC3B,MAAM,WAAW,KAAK,KAAK,MAAM,KAAK;AAGtC,MAAI,MAAM,SAAS,eAAgB;AAEnC,MAAI,MAAM,gBAAgB,EAAE;GAE1B,MAAM,WAAW,MAAM,KAAK,SAAS;AACrC,OAAI,SAAS,aAAa,CACxB,OAAM,KAAK,GAAI,MAAM,aAAa,UAAU,QAAQ,CAAE;YAC7C,SAAS,QAAQ,CAC1B,OAAM,KAAK,SAAS,SAAS,SAAS,CAAC,MAAM,IAAI,CAAC,KAAK,IAAI,CAAC;aAErD,MAAM,aAAa,CAC5B,OAAM,KAAK,GAAI,MAAM,aAAa,UAAU,QAAQ,CAAE;MAEtD,OAAM,KAAK,SAAS,SAAS,SAAS,CAAC,MAAM,IAAI,CAAC,KAAK,IAAI,CAAC;;AAIhE,QAAO;;;;;;;AAQT,eAAsB,oBACpB,WACA,UACuB;CACvB,MAAM,eAAe,MAAM,aAAa,UAAU;CAElD,MAAM,gBAAgB,IAAI,IAAI,OAAO,KAAK,SAAS,MAAM,CAAC;CAE1D,MAAM,WAAqB,EAAE;CAC7B,MAAM,eAAyB,EAAE;AAGjC,OAAM,aAAa,cAAc,iBAAiB,OAAO,aAAa;EACpE,MAAM,OAAO,MAAM,gBAAgB,KAAK,WAAW,SAAS,CAAC;AAE7D,MAAI,CAAC,cAAc,IAAI,SAAS,CAC9B,UAAS,KAAK,SAAS;WACd,SAAS,MAAM,cAAc,KACtC,cAAa,KAAK,SAAS;GAE7B;CAGF,MAAM,aAAa,IAAI,IAAI,aAAa;CACxC,MAAM,eAAe,CAAC,GAAG,cAAc,CAAC,QACrC,aAAa,CAAC,WAAW,IAAI,SAAS,CACxC;AAED,QAAO;EACL,KAAK,CAAC,GAAG,SAAS,CAAC,MAAM;EACzB,SAAS,CAAC,GAAG,aAAa,CAAC,MAAM;EACjC,SAAS,CAAC,GAAG,aAAa,CAAC,MAAM;EAClC;;;;;;;;AAaH,eAAsB,cACpB,WACA,gBACA,cACmB;CAGnB,MAAM,cAAc,MAAM,aAFZ,MAAM,aAAa,UAAU,EAIzC,iBACA,OAAO,aAAa;AAElB,SAAO,CAAC,UADK,MAAM,gBAAgB,KAAK,WAAW,SAAS,CAAC,CACtC;GAE1B;AAGD,aAAY,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,cAAc,EAAE,CAAC;CAElD,MAAM,aAAuC,EAAE;AAC/C,MAAK,MAAM,CAAC,MAAM,SAAS,YACzB,YAAW,QAAQ;AAGrB,QAAO;EACL,YAAY;EACZ,eAAe;EACf,4BAAW,IAAI,MAAM,EAAC,aAAa;EACnC,OAAO;EACR;;;;;;;;AChMH,SAAgB,gBAAgB,QAAgC;CAC9D,MAAM,OAAO,OAAO,QAAQ;CAC5B,MAAM,UAAU,OAAO;CAGvB,IAAI;AACJ,KAAI,WAAW,KACb,iBAAgB,EAAE;UACT,MAAM,QAAQ,QAAQ,CAC/B,iBAAgB;KAEhB,iBAAgB,CAAC,QAAQ;AAG3B,QAAO;EAAE;EAAM,gBAAgB;EAAe;;;;;;AAOhD,SAAgB,iBACd,QACA,eACQ;AACR,KAAI,OAAO,MAAM;EAGf,MAAM,OAAO,OAAO,KAAK,QAAQ,iBAAiB,IAAI;AACtD,MAAI,CAAC,cAAc,IAAI,KAAK,CAC1B,QAAO;AAET,SAAO,WAAW,MAAM,cAAc;;AAExC,QAAO,WAAW,OAAO,QAAQ,WAAW,cAAc;;;;;;AAW5D,SAAgB,eAAe,OAA6B;AAC1D,QAAO;EACL,MAAM,MAAM,QAAQ;EACpB,QAAQ,MAAM,UAAU,EAAE;EAC1B,QAAQ,MAAM,UAAU;EACzB;;;;;AAUH,SAAgB,iBACd,UACqB;CACrB,MAAM,sBAAM,IAAI,KAAqB;AACrC,MAAK,MAAM,CAAC,MAAM,OAAO,OAAO,QAAQ,SAAS,CAC/C,KAAI,IAAI,IAAI,KAAK;AAEnB,QAAO;;;;;;AAOT,SAAgB,yBACd,OACA,gBACuB;AACvB,QAAO,MAAM,KAAK,UAAU;EAC1B,IAAI,KAAK;EACT,MAAM,KAAK,QAAQ;EACnB,OAAO,KAAK,SAAS;EACrB,QAAQ,KAAK,YACR,eAAe,IAAI,KAAK,UAAU,IAAI,OACvC;EACJ,MAAM,KAAK,QAAQ;EACnB,QAAQ,KAAK,UAAU;EACvB,UAAU,KAAK,YAAY;EAC3B,WAAW,KAAK,aAAa;EAC7B,UAAU,yBAAyB,KAAK,YAAY,EAAE,EAAE,eAAe;EACxE,EAAE;;;;;AAML,SAAgB,oBACd,KACA,OACA,gBACiB;AACjB,QAAO;EACL,MAAM,IAAI,QAAQ;EAClB,UAAU,IAAI;EACd,kBAAkB,yBAAyB,OAAO,eAAe;EAClE;;;AAQH,MAAa,6BAEc;;AAG3B,MAAa,wBAEc;;;;;;;;;AAU3B,SAAgB,iBACd,SACA,aACA,eACc;CACd,MAAM,aAAa,QAAQ,aACtB,YAAY,IAAI,QAAQ,WAAW,GAAG,IAAI,OAC3C;CAEJ,MAAM,mBAAmB,QAAQ,oBAC5B,YAAY,IAAI,QAAQ,kBAAkB,GAAG,IAAI,OAClD;CAEJ,MAAM,UAAU,QAAQ,UAAU,EAAE,EACjC,KAAK,MAAM,cAAc,IAAI,EAAE,GAAG,CAAC,CACnC,QAAQ,SAAyB,QAAQ,KAAK;CAEjD,MAAM,cAAc,QAAQ,eAAe,EAAE;AAE7C,QAAO;EACL,MAAM,QAAQ,QAAQ;EACtB,SAAS,QAAQ,WAAW;EAC5B;EACA,mBAAmB;EACnB;EACA,aAAa;GACX,OAAO,YAAY,SAAS,EAAE;GAC9B,OAAO,YAAY,SAAS,EAAE;GAC9B,UAAU,YAAY,YAAY,EAAE;GACpC,WAAW,YAAY,aAAa,EAAE;GACvC;EACF;;;;;;;;;;;;ACnLH,MAAM,aAAa;AACnB,MAAM,kBAAkB;AACxB,MAAM,aAAa;;;;AASnB,SAAS,eAA4B;CACnC,MAAM,QAAQ,cAAc;AAC5B,KAAI,CAAC,OAAO;EACV,MAAM,UAAU,kBAAkB;AAClC,MAAI,CAAC,QACH,OAAM,IAAI,MACR,wBAAwB,MAAM,KAAK,cAAc,GAAG,UACrD;AAEH,QAAM,IAAI,MACR,qCACE,MAAM,KAAK,QAAQ,KAAK,GACxB,WACA,MAAM,KAAK,cAAc,GACzB,uBACH;;AAKH,QAAO,kBAAkB;EACvB,SAHc,QAAQ,IAAI,qBAAqB;EAI/C,oBAAoB;EACrB,CAAC;;;;;AAMJ,eAAe,oBACb,QAC+B;CAC/B,MAAM,MAA4B,EAAE;CACpC,IAAI,OAAO;AAEX,QAAO,MAAM;EACX,MAAM,WAAW,MAAM,QAAQ,qCAC7B,QACA;GACE;GACA,UAAU;GACX,CACF;EACD,MAAM,QAAQ,SAAS,eAAe,EAAE;AACxC,MAAI,KAAK,GAAG,MAAM;EAElB,MAAM,aAAa,SAAS,MAAM,eAAe;AACjD,MAAI,QAAQ,cAAc,MAAM,WAAW,EAAG;AAC9C;;AAGF,QAAO;;AAGT,eAAe,iBACb,QACA,SAC6B;CAC7B,MAAM,cAAc,MAAM,oBAAoB,OAAO;AAErD,KAAI,YAAY,WAAW,EACzB,OAAM,IAAI,MACR,0EACD;AAIH,KAAI,SAAS;EACX,MAAM,QAAQ,YAAY,MACvB,MAAM,EAAE,MAAM,aAAa,KAAK,QAAQ,aAAa,CACvD;AACD,MAAI,CAAC,OAAO;GACV,MAAM,iBAAiB,YACpB,KAAK,MAAM,MAAM,KAAK,EAAE,QAAQ,YAAY,CAAC,CAC7C,KAAK,KAAK;AACb,SAAM,IAAI,MACR,iCAAiC,QAAQ,gBAAgB,iBAC1D;;AAEH,SAAO;;CAIT,MAAM,EAAE,iBAAiB,MAAM,QAAQ;EACrC,MAAM;EACN,MAAM;EACN,SAAS;EACT,SAAS,YAAY,KAAK,OAAO;GAC/B,OAAO,EAAE,QAAQ;GACjB,OAAO,EAAE;GACV,EAAE;EACJ,CAAC;AAEF,KAAI,gBAAgB,KAClB,OAAM,IAAI,MAAM,0BAA0B;CAG5C,MAAM,WAAW,YAAY,MAAM,MAAM,EAAE,OAAO,aAAa;AAC/D,KAAI,CAAC,SACH,OAAM,IAAI,MAAM,iCAAiC;AAGnD,QAAO;;;;;;AAOT,eAAe,sBACb,KACA,iBACA,mBACA,OACkB;CAClB,MAAM,YAAY,KAAK,KAAK,WAAW;CACvC,MAAM,gBAAgB,KAAK,KAAK,gBAAgB;AAGhD,KAAI,CAAC,WAAW,UAAU,CACxB,QAAO;CAIT,MAAM,WAAW,MAAM,aAAa,cAAc;AAClD,KAAI,CAAC,SAEH,QAAO;AAIT,KAAI,SAAS,kBAAkB,gBAC7B,QAAO;CAIT,MAAM,OAAO,MAAM,oBAAoB,WAAW,SAAS;AAI3D,MAFE,KAAK,IAAI,SAAS,KAAK,KAAK,QAAQ,SAAS,KAAK,KAAK,QAAQ,SAAS,MAExD,CAAC,OAAO;AACxB,UAAQ,KAAK;AACb,UAAQ,IACN,MAAM,OAAO,WAAW,GACtB,qBACA,MAAM,KAAK,SAAS,WAAW,GAC/B,SACA,MAAM,KAAK,kBAAkB,GAC7B,gCACH;AACD,UAAQ,KAAK;AACb,UAAQ,IAAI,iBAAiB,KAAK,QAAQ,SAAS,WAAW;AAC9D,UAAQ,IAAI,iBAAiB,KAAK,IAAI,SAAS,WAAW;AAC1D,UAAQ,IAAI,iBAAiB,KAAK,QAAQ,SAAS,WAAW;AAC9D,UAAQ,KAAK;AACb,UAAQ,IACN,SACE,MAAM,KAAK,UAAU,GACrB,oDACH;AACD,SAAO;;AAGT,QAAO;;;;;AAMT,eAAe,gBACb,WACA,cACA,MACe;CACf,MAAM,WAAW,KAAK,WAAW,aAAa;AAE9C,OAAM,MADM,KAAK,UAAU,KAAK,EACf,EAAE,WAAW,MAAM,CAAC;AACrC,OAAM,UAAU,UAAU,KAAK,UAAU,MAAM,MAAM,EAAE,GAAG,MAAM,QAAQ;;AAO1E,MAAa,cAAuB,IAAI,QAAQ,OAAO,CACpD,YACC,wEACD,CACA,OAAO,gBAAgB,+CAA+C,CACtE,OAAO,WAAW,qDAAqD,CACvE,OAAO,OAAO,YAAyB;CACtC,MAAM,MAAM,QAAQ,KAAK;AAEzB,SAAQ,KAAK;AACb,SAAQ,IAAI,MAAM,KAAK,KAAK,oBAAoB,CAAC;AACjD,SAAQ,KAAK;CAGb,MAAM,UAAU,KAAK;AACrB,SAAQ,MAAM,oBAAoB;CAElC,IAAI;AACJ,KAAI;AACF,WAAS,cAAc;AACvB,UAAQ,QAAQ,gBAAgB;UACzB,KAAK;AACZ,UAAQ,KAAK,wBAAwB;AACrC,UAAQ,KAAK;AACb,UAAQ,IACN,MAAM,IAAI,SAAS,GACjB,OACC,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,EACpD;AACD,UAAQ,KAAK;AACb,UAAQ,KAAK,EAAE;;CAIjB,IAAI;AACJ,KAAI;AACF,eAAa,MAAM,iBAAiB,QAAQ,QAAQ,IAAI;UACjD,KAAK;AACZ,UAAQ,KAAK;AACb,UAAQ,IACN,MAAM,IAAI,SAAS,GACjB,OACC,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,EACpD;AACD,UAAQ,KAAK;AACb,UAAQ,KAAK,EAAE;;CAGjB,MAAM,eAAe,WAAW;CAChC,MAAM,iBAAiB,WAAW,QAAQ;AAE1C,SAAQ,IACN,MAAM,KAAK,eAAe,GACxB,MAAM,MAAM,eAAe,GAC3B,MAAM,KAAK,SAAS,aAAa,GAAG,CACvC;AACD,SAAQ,KAAK;CAGb,IAAI;AACJ,KAAI;AACF,eAAa,MAAM,sBACjB,KACA,cACA,gBACA,QAAQ,SAAS,MAClB;UACM,KAAK;AACZ,UAAQ,KAAK;AACb,UAAQ,IACN,MAAM,IAAI,SAAS,GACjB,OACC,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,EACpD;AACD,UAAQ,KAAK;AACb,UAAQ,KAAK,EAAE;;AAEjB,KAAI,CAAC,WACH,SAAQ,KAAK,EAAE;AAIjB,SAAQ,MAAM,wBAAwB;CAEtC,IAAI;CAKJ,IAAI;CAKJ,IAAI;CAKJ,IAAI;CAKJ,IAAI;CAKJ,MAAM,qCAAqB,IAAI,KAG5B;AAEH,KAAI;EACF,MAAM,CACJ,iBACA,gBACA,qBACA,oBACE,MAAM,QAAQ,IAAI;GACpB,QAAQ,iCAAiC,QAAQ,cAAc,EAC7D,UAAU,YACX,CAAC;GACF,QAAQ,gCAAgC,QAAQ,cAAc,EAC5D,UAAU,YACX,CAAC;GACF,QAAQ,qCAAqC,QAAQ,cAAc,EACjE,UAAU,YACX,CAAC;GACF,QAAQ,kCAAkC,QAAQ,cAAc,EAC9D,UAAU,YACX,CAAC;GACH,CAAC;AAEF,iBAAe,gBAAgB,WAAW,EAAE;AAC5C,WAAS,eAAe,UAAU,EAAE;AACpC,gBAAc,oBAAoB,eAAe,EAAE;AACnD,aAAW,iBAAiB,YAAY,EAAE;AAG1C,MAAI,aAAa,WAAW,WAC1B,SAAQ,KACN,MAAM,OACJ,iCAAiC,WAAW,2CAC7C,CACF;AAEH,MAAI,OAAO,WAAW,WACpB,SAAQ,KACN,MAAM,OACJ,gCAAgC,WAAW,0CAC5C,CACF;AAEH,MAAI,YAAY,WAAW,WACzB,SAAQ,KACN,MAAM,OACJ,qCAAqC,WAAW,+CACjD,CACF;AAEH,MAAI,SAAS,WAAW,WACtB,SAAQ,KACN,MAAM,OACJ,kCAAkC,WAAW,4CAC9C,CACF;AAGH,UAAQ,OAAO;EAGf,MAAM,QAAQ,OAAO,GAAG;AAGxB,YAAU,MAAM,QAAQ,IACtB,aAAa,KAAK,MAChB,MAAM,YAAY;GAChB,MAAM,MAAM,MAAM,QAAQ,+BACxB,QACA,cACA,EAAE,GACH;AACD,OAAI,CAAC,IAAI,OACP,OAAM,IAAI,MAAM,yCAAyC,EAAE,KAAK;AAElE,UAAO,IAAI;IACX,CACH,CACF;AAMD,UAAQ,OAAO;AACf,QAAM,QAAQ,IACZ,YAAY,KAAK,QACf,MAAM,YAAY;GAChB,MAAM,MAAM,MAAM,QAAQ,0CACxB,QACA,cACA,IAAI,GACL;AACD,sBAAmB,IAAI,IAAI,IAAI,IAAI,oBAAoB,EAAE,CAAC;IAC1D,CACH,CACF;AAED,UAAQ,QACN,WAAW,QAAQ,OAAO,cAAc,OAAO,OAAO,aAAa,YAAY,OAAO,kBAAkB,SAAS,OAAO,aACzH;UACM,KAAK;AACZ,UAAQ,KAAK,4BAA4B;AACzC,UAAQ,KAAK;AACb,UAAQ,IACN,MAAM,IAAI,SAAS,GACjB,OACC,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,EACpD;AACD,UAAQ,KAAK;AACb,UAAQ,KAAK,EAAE;;AAIjB,SAAQ,MAAM,4BAA4B;CAG1C,MAAM,8BAAc,IAAI,KAAa;CACrC,MAAM,iBAAyC,EAAE;AACjD,MAAK,MAAM,UAAU,SAAS;EAC5B,MAAM,OAAO,iBAAiB,QAAQ,YAAY;AAClD,cAAY,IAAI,KAAK;AACrB,iBAAe,QAAQ,OAAO;;CAIhC,MAAM,6BAAa,IAAI,KAAa;CACpC,MAAM,gBAAwC,EAAE;AAChD,MAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,OAAO,WAAW,MAAM,QAAQ,WAAW,WAAW;AAC5D,aAAW,IAAI,KAAK;AACpB,gBAAc,QAAQ,MAAM;;CAI9B,MAAM,2BAAW,IAAI,KAAa;CAClC,MAAM,cAAsC,EAAE;AAC9C,MAAK,MAAM,OAAO,aAAa;EAC7B,MAAM,OAAO,WAAW,IAAI,QAAQ,WAAW,SAAS;AACxD,WAAS,IAAI,KAAK;AAClB,cAAY,QAAQ,IAAI;;CAI1B,MAAM,+BAAe,IAAI,KAAa;CACtC,MAAM,kBAA0C,EAAE;AAClD,MAAK,MAAM,WAAW,UAAU;EAC9B,MAAM,OAAO,WAAW,QAAQ,QAAQ,WAAW,aAAa;AAChE,eAAa,IAAI,KAAK;AACtB,kBAAgB,QAAQ,QAAQ;;CAIlC,MAAM,iBAAiB,iBAAiB,eAAe;CACvD,MAAM,cAAc,2BAA2B,YAAY;CAC3D,MAAM,gBAAgB,sBAAsB,cAAc;CAC1D,MAAM,kBAAkB,iBAAiB,gBAAgB;AAEzD,SAAQ,QAAQ,wBAAwB;AAGxC,SAAQ,MAAM,mBAAmB;CAEjC,MAAM,YAAY,KAAK,KAAK,WAAW;CACvC,MAAM,gBAAgB,KAAK,KAAK,gBAAgB;CAKhD,MAAM,eAAe,YAAY;AAEjC,KAAI;AAEF,MAAI,WAAW,aAAa,CAC1B,OAAM,GAAG,cAAc,EAAE,WAAW,MAAM,CAAC;AAE7C,QAAM,MAAM,cAAc,EAAE,WAAW,MAAM,CAAC;AAG9C,QAAM,gBAAgB,cAAc,mBAAmB,EACrD,MAAM,gBACP,CAAC;AAGF,OAAK,MAAM,UAAU,SAAS;GAC5B,MAAM,OAAO,eAAe,IAAI,OAAO,GAAG;GAC1C,MAAM,QAAQ,gBAAgB,OAAO;AACrC,SAAM,gBAAgB,cAAc,WAAW,KAAK,QAAQ,MAAM;;AAIpE,OAAK,MAAM,SAAS,QAAQ;GAC1B,MAAM,OAAO,cAAc,IAAI,MAAM,GAAG;GACxC,MAAM,QAAQ,eAAe,MAAM;AACnC,SAAM,gBAAgB,cAAc,UAAU,KAAK,QAAQ,MAAM;;AAInE,OAAK,MAAM,OAAO,aAAa;GAC7B,MAAM,OAAO,YAAY,IAAI,IAAI,GAAG;GAEpC,MAAM,QAAQ,oBAAoB,KADpB,mBAAmB,IAAI,IAAI,GAAG,IAAI,EAAE,EACJ,eAAe;AAC7D,SAAM,gBAAgB,cAAc,eAAe,KAAK,QAAQ,MAAM;;AAIxE,OAAK,MAAM,WAAW,UAAU;GAC9B,MAAM,OAAO,gBAAgB,IAAI,QAAQ,GAAG;AAC5C,OAAI,MAAM;IACR,MAAM,QAAQ,iBAAiB,SAAS,aAAa,cAAc;AACnE,UAAM,gBAAgB,cAAc,YAAY,KAAK,QAAQ,MAAM;;;AAKvE,MAAI,WAAW,UAAU,CACvB,OAAM,GAAG,WAAW,EAAE,WAAW,MAAM,CAAC;AAE1C,QAAM,OAAO,cAAc,UAAU;UAC9B,KAAK;AAEZ,MAAI,WAAW,aAAa,CAC1B,OAAM,GAAG,cAAc,EAAE,WAAW,MAAM,CAAC,CAAC,YAAY,GAAG;AAE7D,UAAQ,KAAK,wBAAwB;AACrC,UAAQ,KAAK;AACb,UAAQ,IACN,MAAM,IAAI,SAAS,GACjB,OACC,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,EACpD;AACD,UAAQ,KAAK;AACb,UAAQ,KAAK,EAAE;;AAOjB,KAAI;AAUF,QAAM,cAAc,eATa;GAC/B,YAAY;IAAE,MAAM;IAAgB,IAAI;IAAc;GACtD,SAAS;GACT,QAAQ;GACR,aAAa;GACb,UAAU;GACV,WAAW,EAAE;GACb,OAAO,EAAE;GACV,CAC2C;AAO5C,QAAM,cAAc,eALH,MAAM,cACrB,WACA,gBACA,aACD,CAC2C;AAE5C,UAAQ,QAAQ,gBAAgB;UACzB,KAAK;AACZ,UAAQ,KAAK,uCAAuC;AACpD,UAAQ,KAAK;AACb,UAAQ,IACN,MAAM,OAAO,WAAW,GACtB,4EAEA,MAAM,KAAK,oBAAoB,GAC/B,iCACH;AACD,UAAQ,IACN,MAAM,KAAK,aAAa,IACrB,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,EACpD;AACD,UAAQ,KAAK;;AAIf,SAAQ,KAAK;AACb,SAAQ,IAAI,MAAM,MAAM,KAAK,iBAAiB,CAAC;AAC/C,SAAQ,KAAK;AACb,SAAQ,IAAI,MAAM,KAAK,2BAA2B,CAAC;AACnD,SAAQ,IACN,MAAM,KAAK,8BAA8B,GACvC,MAAM,MAAM,GAAG,QAAQ,OAAO,UAAU,CAC3C;AACD,SAAQ,IACN,MAAM,KAAK,8BAA8B,GACvC,MAAM,MAAM,GAAG,OAAO,OAAO,UAAU,CAC1C;AACD,SAAQ,IACN,MAAM,KAAK,8BAA8B,GACvC,MAAM,MAAM,GAAG,YAAY,OAAO,UAAU,CAC/C;AACD,SAAQ,IACN,MAAM,KAAK,8BAA8B,GACvC,MAAM,MAAM,GAAG,SAAS,OAAO,UAAU,CAC5C;AACD,SAAQ,IACN,MAAM,KAAK,8BAA8B,GACvC,MAAM,MAAM,gCAAgC,CAC/C;AACD,SAAQ,KAAK;EACb"}