From 93b50254e562d5c4a6ecd2f3988dd13329ea0b73 Mon Sep 17 00:00:00 2001 From: Scrum4Me Agent <30029041+madhura68@users.noreply.github.com> Date: Wed, 13 May 2026 23:28:47 +0200 Subject: [PATCH 1/5] feat(caddy): add Caddyfile TextMate grammar and enable Shiki syntax highlighting Adds lib/grammars/caddyfile.json with scopes for directives, named-matchers (@prefix), placeholders, strings, and comments. Updates /caddy page to use createHighlighter with the local grammar instead of the nginx fallback. Co-Authored-By: Claude Sonnet 4.6 --- app/caddy/page.tsx | 14 ++++--- lib/grammars/caddyfile.json | 83 +++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 5 deletions(-) create mode 100644 lib/grammars/caddyfile.json diff --git a/app/caddy/page.tsx b/app/caddy/page.tsx index d50604c..3e3e411 100644 --- a/app/caddy/page.tsx +++ b/app/caddy/page.tsx @@ -1,6 +1,7 @@ import { redirect } from 'next/navigation' import Link from 'next/link' -import { codeToHtml } from 'shiki' +import { createHighlighter } 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' @@ -16,10 +17,13 @@ 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 createHighlighter({ + themes: ['github-dark'], + // eslint-disable-next-line @typescript-eslint/no-explicit-any + langs: [caddyfileGrammar as any], + }) + configHtml = highlighter.codeToHtml(raw || '# (empty)', { + lang: 'caddyfile', theme: 'github-dark', }) } catch (err) { diff --git a/lib/grammars/caddyfile.json b/lib/grammars/caddyfile.json new file mode 100644 index 0000000..5d545a1 --- /dev/null +++ b/lib/grammars/caddyfile.json @@ -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": "[{}]" + } + } +} From 87f554083dfdb56ea0c234054347e4e1c4d71934 Mon Sep 17 00:00:00 2001 From: Scrum4Me Agent <30029041+madhura68@users.noreply.github.com> Date: Wed, 13 May 2026 23:31:46 +0200 Subject: [PATCH 2/5] refactor(caddy): extract module-level highlighter singleton Replace inline createHighlighter() call with a module-level singleton so the Caddyfile grammar is parsed only once across requests. Add type Highlighter import for proper TypeScript typing. Co-Authored-By: Claude Sonnet 4.6 --- app/caddy/page.tsx | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/app/caddy/page.tsx b/app/caddy/page.tsx index 3e3e411..ff20c4b 100644 --- a/app/caddy/page.tsx +++ b/app/caddy/page.tsx @@ -1,6 +1,6 @@ import { redirect } from 'next/navigation' import Link from 'next/link' -import { createHighlighter } 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' @@ -9,6 +9,18 @@ import CaddyView from './_components/caddy-view' export const dynamic = 'force-dynamic' +let highlighterPromise: Promise | 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') @@ -17,11 +29,7 @@ export default async function CaddyPage() { let configError: string | null = null try { const raw = await execAgent('caddy_show_config') - const highlighter = await createHighlighter({ - themes: ['github-dark'], - // eslint-disable-next-line @typescript-eslint/no-explicit-any - langs: [caddyfileGrammar as any], - }) + const highlighter = await getHighlighter() configHtml = highlighter.codeToHtml(raw || '# (empty)', { lang: 'caddyfile', theme: 'github-dark', From 7d5a7576bf706ad2c84d317a2bc931627f99f3c7 Mon Sep 17 00:00:00 2001 From: Scrum4Me Agent <30029041+madhura68@users.noreply.github.com> Date: Wed, 13 May 2026 23:36:30 +0200 Subject: [PATCH 3/5] feat(deps): install CodeMirror 6 dependencies for live Caddyfile editor Adds codemirror@6.0.2, @codemirror/view@6.42.1, @codemirror/state@6.6.0, @codemirror/language@6.12.3, @codemirror/legacy-modes@6.5.2 and @uiw/react-codemirror@4.25.9. No peer-dep conflicts; @uiw/react-codemirror v4.x is compatible with React 19.2.4 in this project. Co-Authored-By: Claude Sonnet 4.6 --- package-lock.json | 224 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 6 ++ 2 files changed, 230 insertions(+) diff --git a/package-lock.json b/package-lock.json index 51b6a01..0eb4437 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index a9c9481..2770f59 100644 --- a/package.json +++ b/package.json @@ -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", From 97420b93cf35adeab349bdde41f864e37f33d814 Mon Sep 17 00:00:00 2001 From: Scrum4Me Agent <30029041+madhura68@users.noreply.github.com> Date: Wed, 13 May 2026 23:37:30 +0200 Subject: [PATCH 4/5] feat(codemirror): add Caddyfile StreamLanguage mode Defines a minimal StreamLanguage tokenizer for CodeMirror 6 that recognises Caddy directives, named-matchers (@-prefix), comments, strings and braces via cm6 highlight-tags. Co-Authored-By: Claude Sonnet 4.6 --- lib/codemirror/caddyfile-mode.ts | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 lib/codemirror/caddyfile-mode.ts diff --git a/lib/codemirror/caddyfile-mode.ts b/lib/codemirror/caddyfile-mode.ts new file mode 100644 index 0000000..87de6b7 --- /dev/null +++ b/lib/codemirror/caddyfile-mode.ts @@ -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 = { + 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) From 8b72a00127fc2770ac17a32420be9a49996a5317 Mon Sep 17 00:00:00 2001 From: Scrum4Me Agent <30029041+madhura68@users.noreply.github.com> Date: Wed, 13 May 2026 23:39:29 +0200 Subject: [PATCH 5/5] feat(caddy): replace textarea with CodeMirror 6 editor in caddy-editor Replaces the plain textarea on /caddy/edit with a CodeMirror 6 component that provides live Caddyfile syntax highlighting (keywords, named matchers, comments). The editor is dynamically imported (ssr: false) to prevent hydration errors. The write/validate/save/reload state machine and content flow remain unchanged. Bundle impact: ~300 kB additional for the /caddy/edit route (CodeMirror 6 core + @uiw/react-codemirror). Co-Authored-By: Claude Sonnet 4.6 --- app/caddy/_components/caddy-codemirror.tsx | 25 ++++++++++++++++++++++ app/caddy/_components/caddy-editor.tsx | 20 +++++++++++------ 2 files changed, 38 insertions(+), 7 deletions(-) create mode 100644 app/caddy/_components/caddy-codemirror.tsx diff --git a/app/caddy/_components/caddy-codemirror.tsx b/app/caddy/_components/caddy-codemirror.tsx new file mode 100644 index 0000000..15b010c --- /dev/null +++ b/app/caddy/_components/caddy-codemirror.tsx @@ -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 ( + + ) +} diff --git a/app/caddy/_components/caddy-editor.tsx b/app/caddy/_components/caddy-editor.tsx index 14a4427..6d5f0d1 100644 --- a/app/caddy/_components/caddy-editor.tsx +++ b/app/caddy/_components/caddy-editor.tsx @@ -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: () => ( +
+ Loading editor… +
+ ), +}) + type Phase = 'edit' | 'writing' | 'validating' | 'validated' | 'saving' | 'saved' type DialogPending = 'validate' | 'save' | null @@ -106,17 +116,13 @@ export default function CaddyEditor({ initialContent, initialError }: Props) { )} -