Merge pull request #8 from madhura68/feat/sprint-q0rgi4ei

Sprint: fix caddy parser
This commit is contained in:
Janpeter Visser 2026-05-13 22:03:03 +00:00 committed by GitHub
commit e0c2536a8c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 399 additions and 12 deletions

View file

@ -0,0 +1,25 @@
'use client'
import CodeMirror from '@uiw/react-codemirror'
import { caddyfileLanguage } from '@/lib/codemirror/caddyfile-mode'
import { EditorView } from '@codemirror/view'
type Props = {
value: string
onChange: (next: string) => void
readOnly?: boolean
}
export default function CaddyCodeMirror({ value, onChange, readOnly }: Props) {
return (
<CodeMirror
value={value}
onChange={onChange}
readOnly={readOnly}
extensions={[caddyfileLanguage, EditorView.lineWrapping]}
theme="dark"
height="480px"
basicSetup={{ lineNumbers: true, foldGutter: false, highlightActiveLine: !readOnly }}
className="rounded-lg border border-border overflow-hidden text-xs"
/>
)
}

View file

@ -1,11 +1,21 @@
'use client'
import { useCallback, useEffect, useState } from 'react'
import dynamic from 'next/dynamic'
import Link from 'next/link'
import { useFlowRun } from '@/hooks/useFlowRun'
import ConfirmDialog from '@/components/ConfirmDialog'
import StreamingTerminal from '@/components/StreamingTerminal'
const CaddyCodeMirror = dynamic(() => import('./caddy-codemirror'), {
ssr: false,
loading: () => (
<div className="h-[480px] rounded-lg border border-border bg-zinc-950 p-4 text-xs text-zinc-500">
Loading editor
</div>
),
})
type Phase = 'edit' | 'writing' | 'validating' | 'validated' | 'saving' | 'saved'
type DialogPending = 'validate' | 'save' | null
@ -106,17 +116,13 @@ export default function CaddyEditor({ initialContent, initialError }: Props) {
</span>
)}
</div>
<textarea
<CaddyCodeMirror
value={content}
onChange={(e) => {
setContent(e.target.value)
// Reset validated state if user edits after validation
onChange={(next) => {
setContent(next)
if (phase === 'validated' || phase === 'saved') setPhase('edit')
}}
readOnly={isActive}
rows={24}
spellCheck={false}
className="w-full rounded-lg border border-border bg-zinc-950 p-4 font-mono text-xs text-zinc-100 focus:outline-none focus:ring-1 focus:ring-ring resize-y disabled:opacity-50"
/>
</div>

View file

@ -1,6 +1,7 @@
import { redirect } from 'next/navigation'
import Link from 'next/link'
import { codeToHtml } from 'shiki'
import { createHighlighter, type Highlighter } from 'shiki'
import caddyfileGrammar from '@/lib/grammars/caddyfile.json'
import { getCurrentUser } from '@/lib/session'
import { execAgent } from '@/lib/agent-client'
import { parseCertList, type CertInfo } from '@/lib/parse-caddy'
@ -8,6 +9,18 @@ import CaddyView from './_components/caddy-view'
export const dynamic = 'force-dynamic'
let highlighterPromise: Promise<Highlighter> | null = null
function getHighlighter() {
if (!highlighterPromise) {
highlighterPromise = createHighlighter({
themes: ['github-dark'],
// eslint-disable-next-line @typescript-eslint/no-explicit-any
langs: [caddyfileGrammar as any],
})
}
return highlighterPromise
}
export default async function CaddyPage() {
const user = await getCurrentUser()
if (!user) redirect('/login')
@ -16,10 +29,9 @@ export default async function CaddyPage() {
let configError: string | null = null
try {
const raw = await execAgent('caddy_show_config')
// shiki 1.29 bundelt geen caddyfile-grammar; nginx is syntactisch het
// dichtst bij (directives + braces + reverse_proxy lijkt op locations)
configHtml = await codeToHtml(raw || '# (empty)', {
lang: 'nginx',
const highlighter = await getHighlighter()
configHtml = highlighter.codeToHtml(raw || '# (empty)', {
lang: 'caddyfile',
theme: 'github-dark',
})
} catch (err) {

View file

@ -0,0 +1,31 @@
import { StreamLanguage, type StreamParser } from '@codemirror/language'
const CADDY_DIRECTIVES = new Set([
'reverse_proxy', 'encode', 'file_server', 'handle', 'handle_errors',
'root', 'header', 'redir', 'rewrite', 'respond', 'route', 'tls',
'log', 'basicauth', 'request_body', 'try_files', 'php_fastcgi',
'templates', 'import', 'bind', 'metrics', 'admin', 'auto_https',
])
const CADDY_GLOBAL = new Set(['email', 'storage', 'order', 'servers', 'log'])
const parser: StreamParser<unknown> = {
token(stream) {
if (stream.eatSpace()) return null
if (stream.match(/^#.*/)) return 'comment'
if (stream.match(/^"(?:[^"\\]|\\.)*"/)) return 'string'
if (stream.match(/^@[A-Za-z_][\w-]*/)) return 'variableName'
if (stream.match(/^[{}]/)) return 'brace'
const word = stream.match(/^[A-Za-z_][\w.-]*/) as RegExpMatchArray | null
if (word) {
const w = word[0]
if (CADDY_DIRECTIVES.has(w)) return 'keyword'
if (CADDY_GLOBAL.has(w)) return 'typeName'
return 'variableName'
}
stream.next()
return null
},
languageData: { commentTokens: { line: '#' } },
}
export const caddyfileLanguage = StreamLanguage.define(parser)

View file

@ -0,0 +1,83 @@
{
"name": "caddyfile",
"scopeName": "source.Caddyfile",
"fileTypes": ["Caddyfile"],
"patterns": [
{ "include": "#comment" },
{ "include": "#site-address" },
{ "include": "#named-matcher-def" },
{ "include": "#named-matcher-ref" },
{ "include": "#directive" },
{ "include": "#placeholder" },
{ "include": "#string-double" },
{ "include": "#string-backtick" },
{ "include": "#number" },
{ "include": "#braces" }
],
"repository": {
"comment": {
"name": "comment.line.number-sign.caddyfile",
"match": "#.*$"
},
"site-address": {
"name": "entity.name.section.caddyfile",
"match": "^(?:https?://)?[a-zA-Z0-9][a-zA-Z0-9.*-]*(?::[0-9]+)?(?=\\s*(?:\\{|,|$))"
},
"named-matcher-def": {
"name": "entity.other.attribute-name.caddyfile",
"match": "@[a-zA-Z_][a-zA-Z0-9_-]*(?=\\s)"
},
"named-matcher-ref": {
"name": "entity.other.attribute-name.caddyfile",
"match": "(?<=\\s)@[a-zA-Z_][a-zA-Z0-9_-]*"
},
"directive": {
"patterns": [
{
"name": "keyword.control.caddyfile",
"match": "\\b(reverse_proxy|encode|file_server|handle_errors|handle_path|handle|root|header|request_header|response_header|redir|respond|rewrite|uri|try_files|php_fastcgi|push|templates|basicauth|forward_auth|map|vars|log|tls|bind|import|snippet|abort|error|static_response|acme_server|invoke)\\b"
},
{
"name": "support.function.caddyfile",
"match": "\\b(on_demand|off|auto|internal|force|strip_prefix|replace|path_regexp|method|host|header_regexp|remote_ip|client_ip|not|query|cookie|expression|path|protocol|vars_regexp|file|jwt|geo_ip)\\b"
},
{
"name": "keyword.other.option.caddyfile",
"match": "\\b(auto_https|admin|debug|grace_period|shutdown_delay|servers|storage|order|email|acme_ca|acme_ca_root|acme_eab|ocsp_stapling|key_type|cert_issuer|local_certs|skip_install_trust|renew_interval|check_interval|persistent_key|insecure_secrets_log|prefer_wildcard|resolvers|max_size|retention|format|output|level|sampling|include|exclude|dial|upstream|transport|lb_policy|health_uri|health_interval|health_timeout|health_status|health_body|flush_interval|buffer_requests|buffer_responses|max_buffer_size|trusted_proxies|to|from|prefix|replacements|gzip|zstd|br)\\b"
}
]
},
"placeholder": {
"name": "variable.other.caddyfile",
"match": "\\{(?:http\\.(?:request|response|vars|regexp|handlers)|tls|env|vars|system|time|rand|counter|uuid|path|query|header|cookie|form|file|dir|args|blocks|labels|err|http)[^}]*\\}"
},
"string-double": {
"name": "string.quoted.double.caddyfile",
"begin": "\"",
"end": "\"",
"patterns": [
{
"name": "constant.character.escape.caddyfile",
"match": "\\\\."
},
{
"name": "variable.other.caddyfile",
"match": "\\{[^}]+\\}"
}
]
},
"string-backtick": {
"name": "string.quoted.other.caddyfile",
"begin": "`",
"end": "`"
},
"number": {
"name": "constant.numeric.caddyfile",
"match": "\\b[0-9]+(?:\\.[0-9]+)?(?:s|ms|m|h|d|kb|mb|gb)?\\b"
},
"braces": {
"name": "punctuation.section.block.caddyfile",
"match": "[{}]"
}
}
}

224
package-lock.json generated
View file

@ -9,13 +9,19 @@
"version": "0.1.0",
"dependencies": {
"@base-ui/react": "^1.4.1",
"@codemirror/language": "^6.12.3",
"@codemirror/legacy-modes": "^6.5.2",
"@codemirror/state": "^6.6.0",
"@codemirror/view": "^6.42.1",
"@prisma/adapter-pg": "^7.8.0",
"@prisma/client": "^7.8.0",
"@types/bcryptjs": "^2.4.6",
"@types/pg": "^8.20.0",
"@uiw/react-codemirror": "^4.25.9",
"bcryptjs": "^3.0.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"codemirror": "^6.0.2",
"lucide-react": "^1.14.0",
"next": "16.2.6",
"pg": "^8.20.0",
@ -551,6 +557,108 @@
}
}
},
"node_modules/@codemirror/autocomplete": {
"version": "6.20.2",
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.2.tgz",
"integrity": "sha512-G5FPkgIiLjOgZMjqVjvuKQ1rGPtHogLldJr33eFJdVLtmwY+giGrlv/ewljLz6b9BSQLkjxuwBc6g6omDM+YxQ==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.17.0",
"@lezer/common": "^1.0.0"
}
},
"node_modules/@codemirror/commands": {
"version": "6.10.3",
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.3.tgz",
"integrity": "sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.6.0",
"@codemirror/view": "^6.27.0",
"@lezer/common": "^1.1.0"
}
},
"node_modules/@codemirror/language": {
"version": "6.12.3",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.3.tgz",
"integrity": "sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.23.0",
"@lezer/common": "^1.5.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0",
"style-mod": "^4.0.0"
}
},
"node_modules/@codemirror/legacy-modes": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.5.2.tgz",
"integrity": "sha512-/jJbwSTazlQEDOQw2FJ8LEEKVS72pU0lx6oM54kGpL8t/NJ2Jda3CZ4pcltiKTdqYSRk3ug1B3pil1gsjA6+8Q==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0"
}
},
"node_modules/@codemirror/lint": {
"version": "6.9.6",
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.6.tgz",
"integrity": "sha512-6Kp7r6XfCi/D/5sdXieMfg9pJU1bUEx96WITuLU6ESaKizCz0QHFMjY/TaFSbigDdEAIgi93itLBIUETP4oK+A==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.42.0",
"crelt": "^1.0.5"
}
},
"node_modules/@codemirror/search": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.0.tgz",
"integrity": "sha512-ZvGm99wc/s2cITtMT15LFdn8aH/aS+V+DqyGq/N5ZlV5vWtH+nILvC2nw0zX7ByNoHHDZ2IxxdW38O0tc5nVHg==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.37.0",
"crelt": "^1.0.5"
}
},
"node_modules/@codemirror/state": {
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.6.0.tgz",
"integrity": "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==",
"license": "MIT",
"dependencies": {
"@marijn/find-cluster-break": "^1.0.0"
}
},
"node_modules/@codemirror/theme-one-dark": {
"version": "6.1.3",
"resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz",
"integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/highlight": "^1.0.0"
}
},
"node_modules/@codemirror/view": {
"version": "6.42.1",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.42.1.tgz",
"integrity": "sha512-ToN3oFc0nsxNUYVF5P0ztLgbC4UPPjPtA9aKYhkOKQaZASpOUo6ISXyQLP66ctVwlDc+j6Jv0uK5IFALkiXztg==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.6.0",
"crelt": "^1.0.6",
"style-mod": "^4.1.0",
"w3c-keyname": "^2.2.4"
}
},
"node_modules/@cspotcode/source-map-support": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
@ -1406,6 +1514,36 @@
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
"license": "MIT"
},
"node_modules/@lezer/common": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz",
"integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==",
"license": "MIT"
},
"node_modules/@lezer/highlight": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
"integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.3.0"
}
},
"node_modules/@lezer/lr": {
"version": "1.4.10",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz",
"integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.0.0"
}
},
"node_modules/@marijn/find-cluster-break": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
"license": "MIT"
},
"node_modules/@modelcontextprotocol/sdk": {
"version": "1.29.0",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz",
@ -2562,6 +2700,59 @@
"integrity": "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==",
"license": "MIT"
},
"node_modules/@uiw/codemirror-extensions-basic-setup": {
"version": "4.25.9",
"resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.25.9.tgz",
"integrity": "sha512-QFAqr+pu6lDmNpAlecODcF49TlsrZ0bj15zPzfhiqSDl+Um3EsDLFLppixC7kFLn+rdDM2LTvVjn5CPvefpRgw==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/commands": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/lint": "^6.0.0",
"@codemirror/search": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0"
},
"funding": {
"url": "https://jaywcjlove.github.io/#/sponsor"
},
"peerDependencies": {
"@codemirror/autocomplete": ">=6.0.0",
"@codemirror/commands": ">=6.0.0",
"@codemirror/language": ">=6.0.0",
"@codemirror/lint": ">=6.0.0",
"@codemirror/search": ">=6.0.0",
"@codemirror/state": ">=6.0.0",
"@codemirror/view": ">=6.0.0"
}
},
"node_modules/@uiw/react-codemirror": {
"version": "4.25.9",
"resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.25.9.tgz",
"integrity": "sha512-HftqCBUYShAOH0pGi1CHP8vfm5L8fQ3+0j0VI6lQD6QpK+UBu3J7nxfEN5O/BXMilMNf9ZyFJRvRcuMMOLHMng==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.18.6",
"@codemirror/commands": "^6.1.0",
"@codemirror/state": "^6.1.1",
"@codemirror/theme-one-dark": "^6.0.0",
"@uiw/codemirror-extensions-basic-setup": "4.25.9",
"codemirror": "^6.0.0"
},
"funding": {
"url": "https://jaywcjlove.github.io/#/sponsor"
},
"peerDependencies": {
"@babel/runtime": ">=7.11.0",
"@codemirror/state": ">=6.0.0",
"@codemirror/theme-one-dark": ">=6.0.0",
"@codemirror/view": ">=6.0.0",
"codemirror": ">=6.0.0",
"react": ">=17.0.0",
"react-dom": ">=17.0.0"
}
},
"node_modules/@ungap/structured-clone": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz",
@ -3130,6 +3321,21 @@
"integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==",
"license": "MIT"
},
"node_modules/codemirror": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz",
"integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/commands": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/lint": "^6.0.0",
"@codemirror/search": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@ -3269,6 +3475,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/crelt": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
"license": "MIT"
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@ -6953,6 +7165,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/style-mod": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
"license": "MIT"
},
"node_modules/styled-jsx": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
@ -7447,6 +7665,12 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/w3c-keyname": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
"license": "MIT"
},
"node_modules/web-streams-polyfill": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",

View file

@ -13,13 +13,19 @@
},
"dependencies": {
"@base-ui/react": "^1.4.1",
"@codemirror/language": "^6.12.3",
"@codemirror/legacy-modes": "^6.5.2",
"@codemirror/state": "^6.6.0",
"@codemirror/view": "^6.42.1",
"@prisma/adapter-pg": "^7.8.0",
"@prisma/client": "^7.8.0",
"@types/bcryptjs": "^2.4.6",
"@types/pg": "^8.20.0",
"@uiw/react-codemirror": "^4.25.9",
"bcryptjs": "^3.0.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"codemirror": "^6.0.2",
"lucide-react": "^1.14.0",
"next": "16.2.6",
"pg": "^8.20.0",