diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ade254b..e64646d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,7 +19,7 @@ importers: version: 8.56.0(eslint@10.0.1(jiti@2.6.1))(typescript@5.9.3) '@vitest/coverage-v8': specifier: ^4.0.18 - version: 4.0.18(vitest@4.0.18(@types/node@25.3.0)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.18(vitest@4.0.18(@types/node@25.3.0)(jiti@2.6.1)(jsdom@28.1.0)(tsx@4.21.0)(yaml@2.8.2)) eslint: specifier: ^10.0.1 version: 10.0.1(jiti@2.6.1) @@ -37,7 +37,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@types/node@25.3.0)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) + version: 4.0.18(@types/node@25.3.0)(jiti@2.6.1)(jsdom@28.1.0)(tsx@4.21.0)(yaml@2.8.2) src/cli: dependencies: @@ -181,12 +181,107 @@ importers: specifier: ^3.24.0 version: 3.25.76 + src/web: + dependencies: + '@monaco-editor/react': + specifier: ^4.7.0 + version: 4.7.0(monaco-editor@0.55.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: + specifier: ^19.2.5 + version: 19.2.5 + react-dom: + specifier: ^19.2.5 + version: 19.2.5(react@19.2.5) + react-router-dom: + specifier: ^7.7.0 + version: 7.14.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + devDependencies: + '@testing-library/jest-dom': + specifier: ^6.7.0 + version: 6.9.1 + '@testing-library/react': + specifier: ^16.3.0 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@types/react': + specifier: ^19.2.14 + version: 19.2.14 + '@types/react-dom': + specifier: ^19.2.0 + version: 19.2.3(@types/react@19.2.14) + '@vitejs/plugin-react': + specifier: ^5.1.0 + version: 5.2.0(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)) + jsdom: + specifier: ^28.0.0 + version: 28.1.0 + vite: + specifier: ^7.2.0 + version: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) + packages: + '@acemir/cssom@0.9.31': + resolution: {integrity: sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==} + + '@adobe/css-tools@4.4.4': + resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} + '@alcalzone/ansi-tokenize@0.2.5': resolution: {integrity: sha512-3NX/MpTdroi0aKz134A6RC2Gb2iXVECN4QaAXnvCIxxIm3C3AVB1mkUe8NaaiyvOpDfsrqWhYtj+Q6a62RrTsw==} engines: {node: '>=18'} + '@asamuzakjp/css-color@5.1.11': + resolution: {integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/dom-selector@6.8.1': + resolution: {integrity: sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==} + + '@asamuzakjp/generational-cache@1.0.1': + resolution: {integrity: sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.27.1': resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} @@ -195,11 +290,43 @@ packages: resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.2': + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} + engines: {node: '>=6.9.0'} + '@babel/parser@7.29.0': resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} engines: {node: '>=6.0.0'} hasBin: true + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + '@babel/types@7.29.0': resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} @@ -211,6 +338,46 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} + '@bramus/specificity@2.4.2': + resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} + hasBin: true + + '@csstools/color-helpers@6.0.2': + resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==} + engines: {node: '>=20.19.0'} + + '@csstools/css-calc@3.2.0': + resolution: {integrity: sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-color-parser@4.1.0': + resolution: {integrity: sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-parser-algorithms@4.0.0': + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.3': + resolution: {integrity: sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + + '@csstools/css-tokenizer@4.0.0': + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + engines: {node: '>=20.19.0'} + '@esbuild/aix-ppc64@0.27.3': resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} engines: {node: '>=18'} @@ -397,6 +564,15 @@ packages: resolution: {integrity: sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} + '@exodus/bytes@1.15.0': + resolution: {integrity: sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + '@fastify/ajv-compiler@4.0.5': resolution: {integrity: sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==} @@ -600,6 +776,12 @@ packages: '@types/node': optional: true + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -646,6 +828,16 @@ packages: '@cfworker/json-schema': optional: true + '@monaco-editor/loader@1.7.0': + resolution: {integrity: sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==} + + '@monaco-editor/react@4.7.0': + resolution: {integrity: sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==} + peerDependencies: + monaco-editor: '>= 0.25.0 < 1' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} @@ -709,6 +901,9 @@ packages: '@protobufjs/utf8@1.1.0': resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + '@rolldown/pluginutils@1.0.0-rc.3': + resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==} + '@rollup/rollup-android-arm-eabi@4.58.0': resolution: {integrity: sha512-mr0tmS/4FoVk1cnaeN244A/wjvGDNItZKR8hRhnmCzygyRXYtKF5jVDSIILR1U97CTzAYmbgIj/Dukg62ggG5w==} cpu: [arm] @@ -837,6 +1032,44 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} + + '@testing-library/jest-dom@6.9.1': + resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + + '@testing-library/react@16.3.2': + resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/bcrypt@5.0.2': resolution: {integrity: sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==} @@ -880,6 +1113,11 @@ packages: '@types/node@25.3.0': resolution: {integrity: sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==} + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + '@types/react@19.2.14': resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} @@ -889,6 +1127,9 @@ packages: '@types/stream-buffers@3.0.8': resolution: {integrity: sha512-J+7VaHKNvlNPJPEJXX/fKa9DZtR/xPMwuIbe+yNOwp1YB+ApUOBv2aUpEoBJEi8nJgbgs1x8e73ttg0r1rSUdw==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@typescript-eslint/eslint-plugin@8.56.0': resolution: {integrity: sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -948,6 +1189,12 @@ packages: resolution: {integrity: sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@vitejs/plugin-react@5.2.0': + resolution: {integrity: sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + '@vitest/coverage-v8@4.0.18': resolution: {integrity: sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg==} peerDependencies: @@ -1044,6 +1291,10 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + ansi-styles@6.2.3: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} @@ -1059,6 +1310,13 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + asn1@0.2.6: resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} @@ -1142,6 +1400,11 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + baseline-browser-mapping@2.10.23: + resolution: {integrity: sha512-xwVXGqevyKPsiuQdLj+dZMVjidjJV508TBqexND5HrF89cGdCYCJFB3qhcxRHSeMctdCfbR1jrxBajhDy7o29g==} + engines: {node: '>=6.0.0'} + hasBin: true + bcrypt-pbkdf@1.0.2: resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} @@ -1149,6 +1412,9 @@ packages: resolution: {integrity: sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==} engines: {node: '>= 10.0.0'} + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -1166,6 +1432,11 @@ packages: resolution: {integrity: sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==} engines: {node: 20 || >=22} + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} @@ -1193,6 +1464,9 @@ packages: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} + caniuse-lite@1.0.30001791: + resolution: {integrity: sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==} + chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} @@ -1289,6 +1563,9 @@ packages: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + convert-to-spaces@2.0.1: resolution: {integrity: sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -1317,9 +1594,24 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + + cssstyle@6.2.0: + resolution: {integrity: sha512-Fm5NvhYathRnXNVndkUsCCuR63DCLVVwGOOwQw782coXFi5HhkXdu289l59HlXZBawsyNccXfWRYvLzcDCdDig==} + engines: {node: '>=20'} + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + data-urls@7.0.0: + resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -1329,6 +1621,9 @@ packages: supports-color: optional: true + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -1377,6 +1672,15 @@ packages: resolution: {integrity: sha512-iND4mcOWhPaCNh54WmK/KoSb35AFqPAUWFMffTQcp52uQt36b5uNwEJTSXntJZBbeGad72Crbi/hvDIv6us/6Q==} engines: {node: '>= 8.0'} + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + + dompurify@3.2.7: + resolution: {integrity: sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==} + dotenv@16.6.1: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} @@ -1391,6 +1695,9 @@ packages: effect@3.18.4: resolution: {integrity: sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==} + electron-to-chromium@1.5.344: + resolution: {integrity: sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==} + emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} @@ -1408,6 +1715,10 @@ packages: end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + entities@8.0.0: + resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} + engines: {node: '>=20.19.0'} + environment@1.1.0: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} @@ -1645,6 +1956,10 @@ packages: engines: {node: '>=10'} deprecated: This package is no longer supported. + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} @@ -1715,6 +2030,10 @@ packages: resolution: {integrity: sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==} engines: {node: '>=14'} + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -1722,10 +2041,18 @@ packages: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + https-proxy-agent@5.0.1: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + iconv-lite@0.7.2: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} @@ -1745,6 +2072,10 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + indent-string@5.0.0: resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} engines: {node: '>=12'} @@ -1811,6 +2142,9 @@ packages: engines: {node: '>=20'} hasBin: true + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} @@ -1848,14 +2182,31 @@ packages: js-tokens@10.0.0: resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-yaml@4.1.1: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true + jsdom@28.1.0: + resolution: {integrity: sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + jsep@1.4.0: resolution: {integrity: sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==} engines: {node: '>= 10.16.0'} + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -1874,6 +2225,11 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + jsonpath-plus@10.4.0: resolution: {integrity: sha512-T92WWatJXmhBbKsgH/0hl+jxjdXrifi5IKeMY02DWggRxX0UElcbVzPlmgLTbvsPeW1PasQ6xE2Q75stkhGbsA==} engines: {node: '>=18.0.0'} @@ -1903,6 +2259,13 @@ packages: resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==} engines: {node: 20 || >=22} + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -1917,10 +2280,18 @@ packages: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} + marked@14.0.0: + resolution: {integrity: sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==} + engines: {node: '>= 18'} + hasBin: true + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + media-typer@1.1.0: resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} engines: {node: '>= 0.8'} @@ -1949,6 +2320,10 @@ packages: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + minimatch@10.2.2: resolution: {integrity: sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==} engines: {node: 18 || 20 || >=22} @@ -1987,6 +2362,9 @@ packages: mnemonist@0.40.0: resolution: {integrity: sha512-kdd8AFNig2AD5Rkih7EPCXhu/iMvwevQFX/uEiGhZyPZi7fHqOoF4V4kHLpCfysxXMgQ4B52kdPMCwARshKvEg==} + monaco-editor@0.55.1: + resolution: {integrity: sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -2024,6 +2402,9 @@ packages: encoding: optional: true + node-releases@2.0.38: + resolution: {integrity: sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==} + nopt@5.0.0: resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} engines: {node: '>=6'} @@ -2091,6 +2472,9 @@ packages: package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + parse5@8.0.1: + resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -2156,6 +2540,10 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + prisma@6.19.2: resolution: {integrity: sha512-XTKeKxtQElcq3U9/jHyxSPgiRgeYDKxWTPOf6NkXA0dNj5j40MfEsZkMbyNpwDWCUv7YBFUl7I2VK/6ALbmhEg==} engines: {node: '>=18.18'} @@ -2208,16 +2596,49 @@ packages: rc9@2.1.2: resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} + react-dom@19.2.5: + resolution: {integrity: sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==} + peerDependencies: + react: ^19.2.5 + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-reconciler@0.33.0: resolution: {integrity: sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA==} engines: {node: '>=0.10.0'} peerDependencies: react: ^19.2.0 + react-refresh@0.18.0: + resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} + engines: {node: '>=0.10.0'} + + react-router-dom@7.14.2: + resolution: {integrity: sha512-YZcM5ES8jJSM+KrJ9BdvHHqlnGTg5tH3sC5ChFRj4inosKctdyzBDhOyyHdGk597q2OT6NTrCA1OvB/YDwfekQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + react-router@7.14.2: + resolution: {integrity: sha512-yCqNne6I8IB6rVCH7XUvlBK7/QKyqypBFGv+8dj4QBFJiiRX+FG7/nkdAvGElyvVZ/HQP5N19wzteuTARXi5Gw==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + react@19.2.4: resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} engines: {node: '>=0.10.0'} + react@19.2.5: + resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==} + engines: {node: '>=0.10.0'} + readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} @@ -2230,6 +2651,10 @@ packages: resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} engines: {node: '>= 12.13.0'} + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -2298,6 +2723,10 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -2409,6 +2838,9 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + state-local@1.0.7: + resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==} + statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} @@ -2446,10 +2878,17 @@ packages: resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} engines: {node: '>=12'} + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + tagged-tag@1.0.0: resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} engines: {node: '>=20'} @@ -2501,6 +2940,13 @@ packages: resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} engines: {node: '>=14.0.0'} + tldts-core@7.0.28: + resolution: {integrity: sha512-7W5Efjhsc3chVdFhqtaU0KtK32J37Zcr9RKtID54nG+tIpcY79CQK/veYPODxtD/LJ4Lue66jvrQzIX2Z2/pUQ==} + + tldts@7.0.28: + resolution: {integrity: sha512-+Zg3vWhRUv8B1maGSTFdev9mjoo8Etn2Ayfs4cnjlD3CsGkxXX4QyW3j2WJ0wdjYcYmy7Lx2RDsZMhgCWafKIw==} + hasBin: true + toad-cache@3.7.0: resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} engines: {node: '>=12'} @@ -2509,9 +2955,17 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + tough-cookie@6.0.1: + resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} + engines: {node: '>=16'} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + ts-api-utils@2.4.0: resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} engines: {node: '>=18.12'} @@ -2555,10 +3009,20 @@ packages: undici-types@7.18.2: resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + undici@7.25.0: + resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==} + engines: {node: '>=20.18.1'} + unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -2647,9 +3111,25 @@ packages: jsdom: optional: true + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + + whatwg-url@16.0.1: + resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -2701,10 +3181,20 @@ packages: utf-8-validate: optional: true + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} @@ -2742,19 +3232,144 @@ packages: snapshots: + '@acemir/cssom@0.9.31': {} + + '@adobe/css-tools@4.4.4': {} + '@alcalzone/ansi-tokenize@0.2.5': dependencies: ansi-styles: 6.2.3 is-fullwidth-code-point: 5.1.0 + '@asamuzakjp/css-color@5.1.11': + dependencies: + '@asamuzakjp/generational-cache': 1.0.1 + '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@asamuzakjp/dom-selector@6.8.1': + dependencies: + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.2.6 + + '@asamuzakjp/generational-cache@1.0.1': {} + + '@asamuzakjp/nwsapi@2.3.9': {} + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.0': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.28.6': {} + '@babel/helper-string-parser@7.27.1': {} '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.29.2': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + '@babel/parser@7.29.0': dependencies: '@babel/types': 7.29.0 + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/runtime@7.29.2': {} + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + '@babel/types@7.29.0': dependencies: '@babel/helper-string-parser': 7.27.1 @@ -2764,6 +3379,34 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} + '@bramus/specificity@2.4.2': + dependencies: + css-tree: 3.2.1 + + '@csstools/color-helpers@6.0.2': {} + + '@csstools/css-calc@3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-color-parser@4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/color-helpers': 6.0.2 + '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.3(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + + '@csstools/css-tokenizer@4.0.0': {} + '@esbuild/aix-ppc64@0.27.3': optional: true @@ -2872,6 +3515,8 @@ snapshots: '@eslint/core': 1.1.0 levn: 0.4.1 + '@exodus/bytes@1.15.0': {} + '@fastify/ajv-compiler@4.0.5': dependencies: ajv: 8.18.0 @@ -3078,6 +3723,16 @@ snapshots: optionalDependencies: '@types/node': 25.3.0 + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/sourcemap-codec@1.5.5': {} @@ -3163,6 +3818,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@monaco-editor/loader@1.7.0': + dependencies: + state-local: 1.0.7 + + '@monaco-editor/react@4.7.0(monaco-editor@0.55.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@monaco-editor/loader': 1.7.0 + monaco-editor: 0.55.1 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + '@pinojs/redact@0.4.0': {} '@prisma/client@6.19.2(prisma@6.19.2(typescript@5.9.3))(typescript@5.9.3)': @@ -3223,6 +3889,8 @@ snapshots: '@protobufjs/utf8@1.1.0': {} + '@rolldown/pluginutils@1.0.0-rc.3': {} + '@rollup/rollup-android-arm-eabi@4.58.0': optional: true @@ -3300,6 +3968,59 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@testing-library/dom@10.4.1': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/runtime': 7.29.2 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/jest-dom@6.9.1': + dependencies: + '@adobe/css-tools': 4.4.4 + aria-query: 5.3.2 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@babel/runtime': 7.29.2 + '@testing-library/dom': 10.4.1 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@types/aria-query@5.0.4': {} + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.0 + '@types/bcrypt@5.0.2': dependencies: '@types/node': 25.3.0 @@ -3351,6 +4072,10 @@ snapshots: dependencies: undici-types: 7.18.2 + '@types/react-dom@19.2.3(@types/react@19.2.14)': + dependencies: + '@types/react': 19.2.14 + '@types/react@19.2.14': dependencies: csstype: 3.2.3 @@ -3363,6 +4088,9 @@ snapshots: dependencies: '@types/node': 25.3.0 + '@types/trusted-types@2.0.7': + optional: true + '@typescript-eslint/eslint-plugin@8.56.0(@typescript-eslint/parser@8.56.0(eslint@10.0.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -3454,7 +4182,19 @@ snapshots: '@typescript-eslint/types': 8.56.0 eslint-visitor-keys: 5.0.1 - '@vitest/coverage-v8@4.0.18(vitest@4.0.18(@types/node@25.3.0)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))': + '@vitejs/plugin-react@5.2.0(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) + '@rolldown/pluginutils': 1.0.0-rc.3 + '@types/babel__core': 7.20.5 + react-refresh: 0.18.0 + vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) + transitivePeerDependencies: + - supports-color + + '@vitest/coverage-v8@4.0.18(vitest@4.0.18(@types/node@25.3.0)(jiti@2.6.1)(jsdom@28.1.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/utils': 4.0.18 @@ -3466,7 +4206,7 @@ snapshots: obug: 2.1.1 std-env: 3.10.0 tinyrainbow: 3.0.3 - vitest: 4.0.18(@types/node@25.3.0)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) + vitest: 4.0.18(@types/node@25.3.0)(jiti@2.6.1)(jsdom@28.1.0)(tsx@4.21.0)(yaml@2.8.2) '@vitest/expect@4.0.18': dependencies: @@ -3560,6 +4300,8 @@ snapshots: dependencies: color-convert: 2.0.1 + ansi-styles@5.2.0: {} + ansi-styles@6.2.3: {} aproba@2.1.0: {} @@ -3571,6 +4313,12 @@ snapshots: argparse@2.0.1: {} + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + aria-query@5.3.2: {} + asn1@0.2.6: dependencies: safer-buffer: 2.1.2 @@ -3634,6 +4382,8 @@ snapshots: base64-js@1.5.1: {} + baseline-browser-mapping@2.10.23: {} + bcrypt-pbkdf@1.0.2: dependencies: tweetnacl: 0.14.5 @@ -3646,6 +4396,10 @@ snapshots: - encoding - supports-color + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + bl@4.1.0: dependencies: buffer: 5.7.1 @@ -3679,6 +4433,14 @@ snapshots: dependencies: balanced-match: 4.0.3 + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.23 + caniuse-lite: 1.0.30001791 + electron-to-chromium: 1.5.344 + node-releases: 2.0.38 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + buffer@5.7.1: dependencies: base64-js: 1.5.1 @@ -3714,6 +4476,8 @@ snapshots: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 + caniuse-lite@1.0.30001791: {} + chai@6.2.2: {} chalk@5.6.2: {} @@ -3785,6 +4549,8 @@ snapshots: content-type@1.0.5: {} + convert-source-map@2.0.0: {} + convert-to-spaces@2.0.1: {} cookie-signature@1.2.2: {} @@ -3810,12 +4576,35 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + + css.escape@1.5.1: {} + + cssstyle@6.2.0: + dependencies: + '@asamuzakjp/css-color': 5.1.11 + '@csstools/css-syntax-patches-for-csstree': 1.1.3(css-tree@3.2.1) + css-tree: 3.2.1 + lru-cache: 11.2.6 + csstype@3.2.3: {} + data-urls@7.0.0: + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + transitivePeerDependencies: + - '@noble/hashes' + debug@4.4.3: dependencies: ms: 2.1.3 + decimal.js@10.6.0: {} + deep-is@0.1.4: {} deepmerge-ts@7.1.5: {} @@ -3859,6 +4648,14 @@ snapshots: transitivePeerDependencies: - supports-color + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + + dompurify@3.2.7: + optionalDependencies: + '@types/trusted-types': 2.0.7 + dotenv@16.6.1: {} dunder-proto@1.0.1: @@ -3874,6 +4671,8 @@ snapshots: '@standard-schema/spec': 1.1.0 fast-check: 3.23.2 + electron-to-chromium@1.5.344: {} + emoji-regex@10.6.0: {} emoji-regex@8.0.0: {} @@ -3886,6 +4685,8 @@ snapshots: dependencies: once: 1.4.0 + entities@8.0.0: {} + environment@1.1.0: {} es-define-property@1.0.1: {} @@ -4205,6 +5006,8 @@ snapshots: strip-ansi: 6.0.1 wide-align: 1.1.5 + gensync@1.0.0-beta.2: {} + get-caller-file@2.0.5: {} get-east-asian-width@1.5.0: {} @@ -4281,6 +5084,12 @@ snapshots: hpagent@1.2.0: {} + html-encoding-sniffer@6.0.0: + dependencies: + '@exodus/bytes': 1.15.0 + transitivePeerDependencies: + - '@noble/hashes' + html-escaper@2.0.2: {} http-errors@2.0.1: @@ -4291,6 +5100,13 @@ snapshots: statuses: 2.0.2 toidentifier: 1.0.1 + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 @@ -4298,6 +5114,13 @@ snapshots: transitivePeerDependencies: - supports-color + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + iconv-lite@0.7.2: dependencies: safer-buffer: 2.1.2 @@ -4310,6 +5133,8 @@ snapshots: imurmurhash@0.1.4: {} + indent-string@4.0.0: {} + indent-string@5.0.0: {} inflight@1.0.6: @@ -4385,6 +5210,8 @@ snapshots: is-in-ci@2.0.0: {} + is-potential-custom-element-name@1.0.1: {} + is-promise@4.0.0: {} is-unicode-supported@2.1.0: {} @@ -4414,12 +5241,43 @@ snapshots: js-tokens@10.0.0: {} + js-tokens@4.0.0: {} + js-yaml@4.1.1: dependencies: argparse: 2.0.1 + jsdom@28.1.0: + dependencies: + '@acemir/cssom': 0.9.31 + '@asamuzakjp/dom-selector': 6.8.1 + '@bramus/specificity': 2.4.2 + '@exodus/bytes': 1.15.0 + cssstyle: 6.2.0 + data-urls: 7.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + parse5: 8.0.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.1 + undici: 7.25.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + - supports-color + jsep@1.4.0: {} + jsesc@3.1.0: {} + json-buffer@3.0.1: {} json-schema-ref-resolver@3.0.0: @@ -4434,6 +5292,8 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} + json5@2.2.3: {} + jsonpath-plus@10.4.0: dependencies: '@jsep-plugin/assignment': 1.3.0(jsep@1.4.0) @@ -4465,6 +5325,12 @@ snapshots: lru-cache@11.2.6: {} + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lz-string@1.5.0: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -4483,8 +5349,12 @@ snapshots: dependencies: semver: 7.7.4 + marked@14.0.0: {} + math-intrinsics@1.1.0: {} + mdn-data@2.27.1: {} + media-typer@1.1.0: {} merge-descriptors@2.0.0: {} @@ -4503,6 +5373,8 @@ snapshots: mimic-fn@2.1.0: {} + min-indent@1.0.1: {} + minimatch@10.2.2: dependencies: brace-expansion: 5.0.2 @@ -4536,6 +5408,11 @@ snapshots: dependencies: obliterator: 2.0.5 + monaco-editor@0.55.1: + dependencies: + dompurify: 3.2.7 + marked: 14.0.0 + ms@2.1.3: {} mute-stream@2.0.0: {} @@ -4557,6 +5434,8 @@ snapshots: dependencies: whatwg-url: 5.0.0 + node-releases@2.0.38: {} + nopt@5.0.0: dependencies: abbrev: 1.1.1 @@ -4624,6 +5503,10 @@ snapshots: package-json-from-dist@1.0.1: {} + parse5@8.0.1: + dependencies: + entities: 8.0.0 + parseurl@1.3.3: {} patch-console@2.0.0: {} @@ -4685,6 +5568,12 @@ snapshots: prelude-ls@1.2.1: {} + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + prisma@6.19.2(typescript@5.9.3): dependencies: '@prisma/config': 6.19.2 @@ -4747,13 +5636,38 @@ snapshots: defu: 6.1.4 destr: 2.0.5 + react-dom@19.2.5(react@19.2.5): + dependencies: + react: 19.2.5 + scheduler: 0.27.0 + + react-is@17.0.2: {} + react-reconciler@0.33.0(react@19.2.4): dependencies: react: 19.2.4 scheduler: 0.27.0 + react-refresh@0.18.0: {} + + react-router-dom@7.14.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + dependencies: + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + react-router: 7.14.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + + react-router@7.14.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + dependencies: + cookie: 1.1.1 + react: 19.2.5 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 19.2.5(react@19.2.5) + react@19.2.4: {} + react@19.2.5: {} + readable-stream@3.6.2: dependencies: inherits: 2.0.4 @@ -4764,6 +5678,11 @@ snapshots: real-require@0.2.0: {} + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + require-directory@2.1.1: {} require-from-string@2.0.2: {} @@ -4849,6 +5768,10 @@ snapshots: safer-buffer@2.1.2: {} + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + scheduler@0.27.0: {} secure-json-parse@4.1.0: {} @@ -4977,6 +5900,8 @@ snapshots: stackback@0.0.2: {} + state-local@1.0.7: {} + statuses@2.0.2: {} std-env@3.10.0: {} @@ -5021,10 +5946,16 @@ snapshots: dependencies: ansi-regex: 6.2.2 + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + supports-color@7.2.0: dependencies: has-flag: 4.0.0 + symbol-tree@3.2.4: {} + tagged-tag@1.0.0: {} tar-fs@2.1.4: @@ -5104,12 +6035,26 @@ snapshots: tinyrainbow@3.0.3: {} + tldts-core@7.0.28: {} + + tldts@7.0.28: + dependencies: + tldts-core: 7.0.28 + toad-cache@3.7.0: {} toidentifier@1.0.1: {} + tough-cookie@6.0.1: + dependencies: + tldts: 7.0.28 + tr46@0.0.3: {} + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + ts-api-utils@2.4.0(typescript@5.9.3): dependencies: typescript: 5.9.3 @@ -5147,8 +6092,16 @@ snapshots: undici-types@7.18.2: {} + undici@7.25.0: {} + unpipe@1.0.0: {} + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + uri-js@4.4.1: dependencies: punycode: 2.3.1 @@ -5174,7 +6127,7 @@ snapshots: tsx: 4.21.0 yaml: 2.8.2 - vitest@4.0.18(@types/node@25.3.0)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2): + vitest@4.0.18(@types/node@25.3.0)(jiti@2.6.1)(jsdom@28.1.0)(tsx@4.21.0)(yaml@2.8.2): dependencies: '@vitest/expect': 4.0.18 '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)) @@ -5198,6 +6151,7 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.3.0 + jsdom: 28.1.0 transitivePeerDependencies: - jiti - less @@ -5211,8 +6165,24 @@ snapshots: - tsx - yaml + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + webidl-conversions@3.0.1: {} + webidl-conversions@8.0.1: {} + + whatwg-mimetype@5.0.0: {} + + whatwg-url@16.0.1: + dependencies: + '@exodus/bytes': 1.15.0 + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + whatwg-url@5.0.0: dependencies: tr46: 0.0.3 @@ -5259,8 +6229,14 @@ snapshots: ws@8.19.0: {} + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + y18n@5.0.8: {} + yallist@3.1.1: {} + yallist@4.0.0: {} yaml@2.8.2: {} diff --git a/src/web/.gitignore b/src/web/.gitignore new file mode 100644 index 0000000..989dd66 --- /dev/null +++ b/src/web/.gitignore @@ -0,0 +1,4 @@ +dist +node_modules +.vite +*.log diff --git a/src/web/index.html b/src/web/index.html new file mode 100644 index 0000000..fafc024 --- /dev/null +++ b/src/web/index.html @@ -0,0 +1,21 @@ + + + + + + mcpctl — prompt editor + + + +
+ + + diff --git a/src/web/package.json b/src/web/package.json new file mode 100644 index 0000000..ce525d4 --- /dev/null +++ b/src/web/package.json @@ -0,0 +1,28 @@ +{ + "name": "@mcpctl/web", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc --noEmit && vite build", + "preview": "vite preview", + "test": "vitest", + "test:run": "vitest run" + }, + "dependencies": { + "@monaco-editor/react": "^4.7.0", + "react": "^19.2.5", + "react-dom": "^19.2.5", + "react-router-dom": "^7.7.0" + }, + "devDependencies": { + "@testing-library/jest-dom": "^6.7.0", + "@testing-library/react": "^16.3.0", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.0", + "@vitejs/plugin-react": "^5.1.0", + "jsdom": "^28.0.0", + "vite": "^7.2.0" + } +} diff --git a/src/web/src/App.tsx b/src/web/src/App.tsx new file mode 100644 index 0000000..7981fa7 --- /dev/null +++ b/src/web/src/App.tsx @@ -0,0 +1,42 @@ +import * as React from 'react'; +import { useState, useEffect } from 'react'; +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; +import { getToken } from './api'; +import { Layout } from './components/Layout'; +import { Login } from './components/Login'; +import { ProjectsPage } from './pages/Projects'; +import { ProjectPromptsPage } from './pages/ProjectPrompts'; +import { AgentsPage } from './pages/Agents'; +import { AgentDetailPage } from './pages/AgentDetail'; +import { PersonalityDetailPage } from './pages/PersonalityDetail'; + +export function App(): React.JSX.Element { + const [tokenPresent, setTokenPresent] = useState(getToken() !== null); + + // Listen for storage changes from other tabs. + useEffect(() => { + const onStorage = (): void => setTokenPresent(getToken() !== null); + window.addEventListener('storage', onStorage); + return () => window.removeEventListener('storage', onStorage); + }, []); + + if (!tokenPresent) { + return setTokenPresent(true)} />; + } + + return ( + + + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + ); +} diff --git a/src/web/src/api.ts b/src/web/src/api.ts new file mode 100644 index 0000000..9581938 --- /dev/null +++ b/src/web/src/api.ts @@ -0,0 +1,103 @@ +/** + * Thin fetch wrapper over mcpd's HTTP API. + * + * Reads the bearer token from localStorage on every request — the user can + * paste either a session token (from `mcpctl auth login`) or a PAT + * (`mcpctl_pat_*`). Both flow through the same `Authorization: Bearer …` + * header that mcpd already accepts (see `src/mcpd/src/middleware/auth.ts`). + * + * The wrapper deliberately stays minimal — no caching, no retry policy, no + * cancellation tokens. Add those when a real call site needs them. + */ + +const TOKEN_KEY = 'mcpctl.token'; + +export function getToken(): string | null { + return localStorage.getItem(TOKEN_KEY); +} + +export function setToken(token: string): void { + localStorage.setItem(TOKEN_KEY, token); +} + +export function clearToken(): void { + localStorage.removeItem(TOKEN_KEY); +} + +export interface ApiError extends Error { + status: number; + body: unknown; +} + +async function request(method: string, path: string, body?: unknown): Promise { + const token = getToken(); + const headers: Record = { 'Content-Type': 'application/json' }; + if (token !== null) headers['Authorization'] = `Bearer ${token}`; + + const init: RequestInit = { method, headers }; + if (body !== undefined) init.body = JSON.stringify(body); + const res = await fetch(path, init); + + if (!res.ok) { + let parsed: unknown = null; + try { parsed = await res.json(); } catch { /* ignore */ } + const err = new Error(`HTTP ${String(res.status)} ${res.statusText}`) as ApiError; + err.status = res.status; + err.body = parsed; + throw err; + } + if (res.status === 204) return undefined as T; + return res.json() as Promise; +} + +export const api = { + get: (path: string): Promise => request('GET', path), + post: (path: string, body: unknown): Promise => request('POST', path, body), + put: (path: string, body: unknown): Promise => request('PUT', path, body), + delete: (path: string): Promise => request('DELETE', path), +}; + +// ── Domain types (subset of what the UI needs from mcpd) ── + +export interface Project { + id: string; + name: string; + description?: string; +} + +export interface Agent { + id: string; + name: string; + description: string; + systemPrompt: string; + llm: { id: string; name: string }; + project: { id: string; name: string } | null; + defaultPersonality: { id: string; name: string } | null; +} + +export interface Prompt { + id: string; + name: string; + content: string; + projectId: string | null; + agentId: string | null; + priority: number; + linkTarget: string | null; +} + +export interface Personality { + id: string; + name: string; + description: string; + agentId: string; + agentName: string; + priority: number; + promptCount: number; +} + +export interface PersonalityPrompt { + promptId: string; + promptName: string; + promptContent: string; + priority: number; +} diff --git a/src/web/src/components/Layout.tsx b/src/web/src/components/Layout.tsx new file mode 100644 index 0000000..b951f9e --- /dev/null +++ b/src/web/src/components/Layout.tsx @@ -0,0 +1,80 @@ +import * as React from 'react'; +import { NavLink, Outlet } from 'react-router-dom'; +import { clearToken } from '../api'; + +/** + * Top-of-page nav + outlet. Terminal-style dark theme so the UI feels + * adjacent to the CLI rather than a separate product. + */ +export function Layout(): React.JSX.Element { + return ( +
+
+
mcpctl · prompt editor
+ +
+
+ +
+
+ ); +} + +function navStyle({ isActive }: { isActive: boolean }): React.CSSProperties { + return { + color: isActive ? '#58a6ff' : '#c9d1d9', + textDecoration: 'none', + padding: '6px 12px', + borderBottom: isActive ? '2px solid #58a6ff' : '2px solid transparent', + }; +} + +const styles: Record = { + shell: { + minHeight: '100vh', + display: 'flex', + flexDirection: 'column', + }, + header: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + padding: '12px 24px', + background: '#161b22', + borderBottom: '1px solid #30363d', + }, + brand: { + fontFamily: 'ui-monospace, "SF Mono", Menlo, monospace', + fontWeight: 700, + fontSize: 16, + }, + dim: { color: '#7d8590', fontWeight: 400 }, + nav: { + display: 'flex', + gap: 8, + alignItems: 'center', + }, + logout: { + background: 'transparent', + color: '#c9d1d9', + border: '1px solid #30363d', + padding: '4px 12px', + borderRadius: 4, + cursor: 'pointer', + marginLeft: 12, + }, + main: { + flex: 1, + padding: 24, + overflowY: 'auto', + }, +}; diff --git a/src/web/src/components/Login.tsx b/src/web/src/components/Login.tsx new file mode 100644 index 0000000..0261442 --- /dev/null +++ b/src/web/src/components/Login.tsx @@ -0,0 +1,120 @@ +import * as React from 'react'; +import { useState } from 'react'; +import { setToken } from '../api'; + +/** + * Login screen — paste a session token (`mcpctl auth login` writes one to + * ~/.mcpctl/credentials.json) or a PAT (`mcpctl_pat_*`). The token lives in + * localStorage; logout wipes it. We don't validate the token shape here — + * the first API call will 401 if it's wrong, and the user re-enters. + */ +export function Login({ onLogin }: { onLogin: () => void }): React.JSX.Element { + const [value, setValue] = useState(''); + const [showHelp, setShowHelp] = useState(false); + + function submit(e: React.FormEvent): void { + e.preventDefault(); + if (value.trim() === '') return; + setToken(value.trim()); + onLogin(); + } + + return ( +
+
+

mcpctl prompt editor

+

Paste a session token or PAT.

+ setValue(e.target.value)} + style={styles.input} + /> + + + {showHelp && ( +
+{`# Use your interactive session token (writes to ~/.mcpctl/credentials.json)
+mcpctl auth login
+cat ~/.mcpctl/credentials.json    # field: \`token\`
+
+# Or mint a PAT for a specific project (longer-lived)
+mcpctl create mcptoken my-editor --project my-project
+`}
+          
+ )} +
+
+ ); +} + +const styles: Record = { + shell: { + minHeight: '100vh', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + padding: 24, + }, + card: { + width: '100%', + maxWidth: 420, + background: '#161b22', + border: '1px solid #30363d', + borderRadius: 6, + padding: 32, + display: 'flex', + flexDirection: 'column', + gap: 12, + }, + title: { + margin: 0, + fontFamily: 'ui-monospace, "SF Mono", Menlo, monospace', + fontWeight: 700, + fontSize: 20, + }, + dim: { color: '#7d8590', fontWeight: 400 }, + hint: { margin: 0, color: '#7d8590' }, + input: { + background: '#0d1117', + color: '#c9d1d9', + border: '1px solid #30363d', + borderRadius: 4, + padding: '8px 12px', + fontFamily: 'ui-monospace, "SF Mono", Menlo, monospace', + }, + button: { + background: '#238636', + color: '#fff', + border: 'none', + borderRadius: 4, + padding: '8px 12px', + cursor: 'pointer', + fontWeight: 600, + }, + linkButton: { + background: 'transparent', + color: '#58a6ff', + border: 'none', + cursor: 'pointer', + textAlign: 'left', + padding: 0, + }, + help: { + background: '#0d1117', + color: '#c9d1d9', + padding: 12, + borderRadius: 4, + fontSize: 12, + overflowX: 'auto', + margin: 0, + }, +}; diff --git a/src/web/src/components/PromptEditor.tsx b/src/web/src/components/PromptEditor.tsx new file mode 100644 index 0000000..0729bc4 --- /dev/null +++ b/src/web/src/components/PromptEditor.tsx @@ -0,0 +1,155 @@ +import * as React from 'react'; +import { useState } from 'react'; +import Editor from '@monaco-editor/react'; + +/** + * Inline prompt editor: name + priority + Monaco for content. + * Used by ProjectPrompts, AgentDetail (direct prompts tab), and + * PersonalityDetail (binding-attached prompts). + * + * The component is intentionally dumb — it owns no I/O. The parent does the + * POST/PUT and decides whether to show this in "create" or "edit" mode. + */ +export interface PromptDraft { + name: string; + priority: number; + content: string; +} + +export interface PromptEditorProps { + initial?: Partial; + /** Lock the name field — used when editing existing prompts (name is the unique key). */ + nameLocked?: boolean; + submitLabel: string; + onSubmit: (draft: PromptDraft) => Promise | void; + onCancel?: () => void; + busy?: boolean; +} + +export function PromptEditor(props: PromptEditorProps): React.JSX.Element { + const [name, setName] = useState(props.initial?.name ?? ''); + const [priority, setPriority] = useState(props.initial?.priority ?? 5); + const [content, setContent] = useState(props.initial?.content ?? ''); + const [error, setError] = useState(null); + + async function handleSubmit(e: React.FormEvent): Promise { + e.preventDefault(); + setError(null); + if (!/^[a-z0-9-]+$/.test(name)) { + setError('Name must be lowercase alphanumeric with hyphens (e.g., "tone-rules").'); + return; + } + if (content.trim().length === 0) { + setError('Content is required.'); + return; + } + try { + await props.onSubmit({ name, priority, content }); + } catch (err) { + setError((err as Error).message); + } + } + + return ( +
+
+ + +
+
+ setContent(v ?? '')} + options={{ + minimap: { enabled: false }, + wordWrap: 'on', + fontSize: 13, + automaticLayout: true, + }} + /> +
+ {error !== null &&
{error}
} +
+ + {props.onCancel !== undefined && ( + + )} +
+
+ ); +} + +const styles: Record = { + form: { display: 'flex', flexDirection: 'column', gap: 12 }, + row: { display: 'flex', gap: 16, alignItems: 'flex-end' }, + label: { + display: 'flex', + flexDirection: 'column', + gap: 4, + fontSize: 12, + color: '#7d8590', + flex: 1, + }, + input: { + background: '#0d1117', + color: '#c9d1d9', + border: '1px solid #30363d', + borderRadius: 4, + padding: '6px 10px', + fontFamily: 'ui-monospace, "SF Mono", Menlo, monospace', + }, + editorShell: { border: '1px solid #30363d', borderRadius: 4, overflow: 'hidden' }, + error: { + background: '#2d1416', + color: '#ff7b72', + border: '1px solid #f85149', + padding: '8px 12px', + borderRadius: 4, + fontSize: 13, + }, + actions: { display: 'flex', gap: 8 }, + primary: { + background: '#238636', + color: '#fff', + border: 'none', + borderRadius: 4, + padding: '8px 16px', + cursor: 'pointer', + fontWeight: 600, + }, + secondary: { + background: 'transparent', + color: '#c9d1d9', + border: '1px solid #30363d', + borderRadius: 4, + padding: '8px 16px', + cursor: 'pointer', + }, +}; diff --git a/src/web/src/hooks/useFetch.ts b/src/web/src/hooks/useFetch.ts new file mode 100644 index 0000000..bf26ab3 --- /dev/null +++ b/src/web/src/hooks/useFetch.ts @@ -0,0 +1,35 @@ +import { useEffect, useState, useCallback } from 'react'; + +/** + * Minimal SWR-style hook: call `fn`, expose `{ data, error, loading, refetch }`. + * No global cache — each consumer fetches its own copy. Add caching when you + * see the same query firing 5+ times across mounted components. + */ +export interface UseFetchResult { + data: T | null; + error: Error | null; + loading: boolean; + refetch: () => void; +} + +export function useFetch(fn: () => Promise, deps: unknown[] = []): UseFetchResult { + const [data, setData] = useState(null); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(true); + const [bump, setBump] = useState(0); + + const refetch = useCallback(() => setBump((b) => b + 1), []); + + useEffect(() => { + let cancelled = false; + setLoading(true); + setError(null); + fn() + .then((d) => { if (!cancelled) { setData(d); setLoading(false); } }) + .catch((e: Error) => { if (!cancelled) { setError(e); setLoading(false); } }); + return () => { cancelled = true; }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [bump, ...deps]); + + return { data, error, loading, refetch }; +} diff --git a/src/web/src/main.tsx b/src/web/src/main.tsx new file mode 100644 index 0000000..dd91f32 --- /dev/null +++ b/src/web/src/main.tsx @@ -0,0 +1,11 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { App } from './App'; + +const root = document.getElementById('root'); +if (root === null) throw new Error('#root not found'); +createRoot(root).render( + + + , +); diff --git a/src/web/src/pages/AgentDetail.tsx b/src/web/src/pages/AgentDetail.tsx new file mode 100644 index 0000000..651263f --- /dev/null +++ b/src/web/src/pages/AgentDetail.tsx @@ -0,0 +1,317 @@ +import * as React from 'react'; +import { useState } from 'react'; +import { useParams, Link } from 'react-router-dom'; +import { api, type Agent, type Prompt, type Personality } from '../api'; +import { useFetch } from '../hooks/useFetch'; +import { PromptEditor, type PromptDraft } from '../components/PromptEditor'; + +/** + * Agent detail page. Two tabs: + * - Direct prompts (Prompt.agentId === agent.id) — always-on overlay. + * - Personalities — list, create, click-through to bind prompts. + * + * Why both on one page: an agent's "what it does" lives in this triple + * (systemPrompt + direct prompts + personalities). Splitting them across + * routes hides the relationship that the chat engine cares about. + */ +type Tab = 'direct' | 'personalities'; + +export function AgentDetailPage(): React.JSX.Element { + const { name } = useParams<{ name: string }>(); + const agentName = name ?? ''; + const { data: agent, error: agentError, loading: agentLoading } = useFetch( + () => api.get(`/api/v1/agents/${encodeURIComponent(agentName)}`), + [agentName], + ); + const [tab, setTab] = useState('direct'); + + if (agentLoading) return
Loading agent…
; + if (agentError !== null) return
Error: {agentError.message}
; + if (agent === null) return
Not found.
; + + return ( +
+

+ ← Agents +

+

+ {agent.name} + + {agent.llm.name}{agent.project !== null && ` · ${agent.project.name}`} + +

+ {agent.systemPrompt !== '' && ( +
+ System prompt +
{agent.systemPrompt}
+
+ )} +
+ setTab('direct')}>Direct prompts + setTab('personalities')}>Personalities +
+ {tab === 'direct' && } + {tab === 'personalities' && } +
+ ); +} + +function TabButton({ active, onClick, children }: { active: boolean; onClick: () => void; children: React.ReactNode }): React.JSX.Element { + return ( + + ); +} + +// ── Direct prompts tab ── + +function DirectPromptsTab({ agentName }: { agentName: string }): React.JSX.Element { + const { data, error, loading, refetch } = useFetch( + () => api.get(`/api/v1/agents/${encodeURIComponent(agentName)}/prompts`), + [agentName], + ); + const [creating, setCreating] = useState(false); + const [busy, setBusy] = useState(false); + + if (loading) return
Loading…
; + if (error !== null) return
Error: {error.message}
; + const prompts = data ?? []; + + async function handleCreate(draft: PromptDraft): Promise { + setBusy(true); + try { + await api.post('/api/v1/prompts', { + name: draft.name, + content: draft.content, + priority: draft.priority, + agent: agentName, + }); + setCreating(false); + refetch(); + } finally { + setBusy(false); + } + } + + async function handleDelete(id: string, name: string): Promise { + if (!confirm(`Delete prompt '${name}'?`)) return; + await api.delete(`/api/v1/prompts/${id}`); + refetch(); + } + + return ( +
+

+ Always-on prompts for this agent. Injected after the agent's system prompt and before project prompts. +

+ {!creating && ( + + )} + {creating && ( +
+

New direct prompt

+ setCreating(false)} + busy={busy} + /> +
+ )} + {prompts.length === 0 && !creating &&

No direct prompts yet.

} +
    + {prompts.sort((a, b) => b.priority - a.priority).map((p) => ( +
  • +
    +
    + {p.name} + priority {p.priority} +
    + +
    +
    {p.content}
    +
  • + ))} +
+
+ ); +} + +// ── Personalities tab ── + +function PersonalitiesTab({ agentName }: { agentName: string }): React.JSX.Element { + const { data, error, loading, refetch } = useFetch( + () => api.get(`/api/v1/agents/${encodeURIComponent(agentName)}/personalities`), + [agentName], + ); + const [creating, setCreating] = useState(false); + const [name, setName] = useState(''); + const [description, setDescription] = useState(''); + const [busy, setBusy] = useState(false); + + if (loading) return
Loading…
; + if (error !== null) return
Error: {error.message}
; + const personalities = data ?? []; + + async function handleCreate(e: React.FormEvent): Promise { + e.preventDefault(); + if (!/^[a-z0-9-]+$/.test(name)) { + alert('Name must be lowercase alphanumeric with hyphens.'); + return; + } + setBusy(true); + try { + await api.post(`/api/v1/agents/${encodeURIComponent(agentName)}/personalities`, { + name, description, + }); + setName(''); + setDescription(''); + setCreating(false); + refetch(); + } catch (err) { + alert((err as Error).message); + } finally { + setBusy(false); + } + } + + async function handleDelete(id: string, name: string): Promise { + if (!confirm(`Delete personality '${name}'?`)) return; + await api.delete(`/api/v1/personalities/${id}`); + refetch(); + } + + return ( +
+

+ Named overlays of prompts. Pick at chat time with --personality <name> or set + a default on the agent. +

+ {!creating && ( + + )} + {creating && ( +
+

New personality

+
+ + +
+
+ + +
+
+ )} + {personalities.length === 0 && !creating &&

No personalities yet.

} +
    + {personalities.map((p) => ( +
  • +
    +
    + + {p.name} + + + {p.promptCount} prompt{p.promptCount === 1 ? '' : 's'} + +
    + +
    + {p.description !== '' &&
    {p.description}
    } +
  • + ))} +
+
+ ); +} + +const card: React.CSSProperties = { + background: '#161b22', + border: '1px solid #30363d', + borderRadius: 6, + padding: 16, + margin: '12px 0', +}; +const cardHeader: React.CSSProperties = { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 8, +}; +const contentPre: React.CSSProperties = { + background: '#0d1117', + color: '#c9d1d9', + padding: 12, + borderRadius: 4, + margin: 0, + whiteSpace: 'pre-wrap', + fontFamily: 'ui-monospace, "SF Mono", Menlo, monospace', + fontSize: 12, +}; +const input: React.CSSProperties = { + background: '#0d1117', + color: '#c9d1d9', + border: '1px solid #30363d', + borderRadius: 4, + padding: '6px 10px', + fontFamily: 'ui-monospace, "SF Mono", Menlo, monospace', +}; +const primaryBtn: React.CSSProperties = { + background: '#238636', + color: '#fff', + border: 'none', + borderRadius: 4, + padding: '8px 16px', + cursor: 'pointer', + fontWeight: 600, +}; +const secondaryBtn: React.CSSProperties = { + background: 'transparent', + color: '#c9d1d9', + border: '1px solid #30363d', + borderRadius: 4, + padding: '8px 16px', + cursor: 'pointer', +}; +const dangerBtn: React.CSSProperties = { + background: 'transparent', + color: '#ff7b72', + border: '1px solid #f85149', + borderRadius: 4, + padding: '4px 12px', + cursor: 'pointer', +}; diff --git a/src/web/src/pages/Agents.tsx b/src/web/src/pages/Agents.tsx new file mode 100644 index 0000000..11b1a87 --- /dev/null +++ b/src/web/src/pages/Agents.tsx @@ -0,0 +1,40 @@ +import * as React from 'react'; +import { Link } from 'react-router-dom'; +import { api, type Agent } from '../api'; +import { useFetch } from '../hooks/useFetch'; + +export function AgentsPage(): React.JSX.Element { + const { data, error, loading } = useFetch(() => api.get('/api/v1/agents')); + + if (loading) return
Loading agents…
; + if (error !== null) return
Error: {error.message}
; + const agents = data ?? []; + + return ( +
+

Agents

+

Pick an agent to manage its direct prompts and personalities.

+ {agents.length === 0 &&

No agents yet — create one with mcpctl create agent.

} +
    + {agents.map((a) => ( +
  • + + {a.name} + +
    + LLM: {a.llm.name} + {a.project !== null && <> · Project: {a.project.name}} + {a.defaultPersonality !== null && <> · Default personality: {a.defaultPersonality.name}} +
    + {a.description !== '' && ( +
    {a.description}
    + )} +
  • + ))} +
+
+ ); +} diff --git a/src/web/src/pages/PersonalityDetail.tsx b/src/web/src/pages/PersonalityDetail.tsx new file mode 100644 index 0000000..dc4be8e --- /dev/null +++ b/src/web/src/pages/PersonalityDetail.tsx @@ -0,0 +1,224 @@ +import * as React from 'react'; +import { useState } from 'react'; +import { useParams, Link } from 'react-router-dom'; +import { api, type Personality, type PersonalityPrompt, type Prompt } from '../api'; +import { useFetch } from '../hooks/useFetch'; + +/** + * Personality detail: show metadata, list bound prompts (with priority + * within this overlay), and offer an "attach prompt" picker that lets the + * user select from in-scope candidates (agent-direct + same-project + global). + */ +export function PersonalityDetailPage(): React.JSX.Element { + const { id } = useParams<{ id: string }>(); + const personalityId = id ?? ''; + + const personality = useFetch( + () => api.get(`/api/v1/personalities/${encodeURIComponent(personalityId)}`), + [personalityId], + ); + const bindings = useFetch( + () => api.get(`/api/v1/personalities/${encodeURIComponent(personalityId)}/prompts`), + [personalityId], + ); + + if (personality.loading || bindings.loading) return
Loading…
; + if (personality.error !== null) return
Error: {personality.error.message}
; + if (personality.data === null) return
Not found.
; + + const p = personality.data; + const bound = bindings.data ?? []; + + return ( +
+

+ + ← {p.agentName} + +

+

+ {p.name} + personality on {p.agentName} +

+ {p.description !== '' &&

{p.description}

} + +

Bound prompts ({bound.length})

+

+ Activated when this personality is selected at chat time. Priority controls order within + the overlay (higher first). +

+ {bound.length === 0 &&

No bound prompts yet.

} +
    + {bound + .slice() + .sort((a, b) => b.priority - a.priority) + .map((b) => ( +
  • +
    +
    + {b.promptName} + priority {b.priority} +
    + +
    +
    {b.promptContent}
    +
  • + ))} +
+ + b.promptId)} + onAttached={bindings.refetch} + /> +
+ ); +} + +async function detach( + personalityId: string, + promptId: string, + promptName: string, + refetch: () => void, +): Promise { + if (!confirm(`Detach prompt '${promptName}' from this personality?`)) return; + await api.delete(`/api/v1/personalities/${personalityId}/prompts/${promptId}`); + refetch(); +} + +// ── Attach picker ── + +function AttachPromptPanel(props: { + personality: Personality; + boundPromptIds: string[]; + onAttached: () => void; +}): React.JSX.Element { + const [open, setOpen] = useState(false); + const candidates = useFetch( + async () => { + // In-scope candidates: agent-direct + same-project + global. + const direct = await api.get( + `/api/v1/agents/${encodeURIComponent(props.personality.agentName)}/prompts`, + ); + const agentRow = await api.get<{ project: { name: string } | null }>( + `/api/v1/agents/${encodeURIComponent(props.personality.agentName)}`, + ); + let projectAndGlobal: Prompt[] = []; + if (agentRow.project !== null) { + projectAndGlobal = await api.get( + `/api/v1/prompts?project=${encodeURIComponent(agentRow.project.name)}`, + ); + } else { + projectAndGlobal = await api.get(`/api/v1/prompts?scope=global`); + } + // Dedupe; exclude ones already bound. + const seen = new Set(props.boundPromptIds); + const merged: Prompt[] = []; + for (const p of [...direct, ...projectAndGlobal]) { + if (seen.has(p.id)) continue; + seen.add(p.id); + merged.push(p); + } + return merged; + }, + [props.personality.id, props.boundPromptIds.join(',')], + ); + + const [busyId, setBusyId] = useState(null); + async function attach(promptId: string): Promise { + setBusyId(promptId); + try { + await api.post(`/api/v1/personalities/${props.personality.id}/prompts`, { promptId }); + props.onAttached(); + } catch (err) { + alert((err as Error).message); + } finally { + setBusyId(null); + } + } + + if (!open) { + return ; + } + + return ( +
+

Attach a prompt

+

+ Eligible: prompts on this agent, prompts in the agent's project, or globals. +

+ {candidates.loading &&
Loading candidates…
} + {candidates.error !== null &&
Error: {candidates.error.message}
} +
    + {(candidates.data ?? []).map((p) => ( +
  • +
    + {p.name} + + {p.agentId !== null ? 'agent-direct' : p.projectId !== null ? 'project' : 'global'} + +
    + +
  • + ))} + {(candidates.data ?? []).length === 0 && !candidates.loading && ( +
  • No eligible prompts to attach.
  • + )} +
+ +
+ ); +} + +const card: React.CSSProperties = { + background: '#161b22', + border: '1px solid #30363d', + borderRadius: 6, + padding: 16, + margin: '12px 0', +}; +const cardHeader: React.CSSProperties = { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 8, +}; +const contentPre: React.CSSProperties = { + background: '#0d1117', + color: '#c9d1d9', + padding: 12, + borderRadius: 4, + margin: 0, + whiteSpace: 'pre-wrap', + fontFamily: 'ui-monospace, "SF Mono", Menlo, monospace', + fontSize: 12, +}; +const primaryBtn: React.CSSProperties = { + background: '#238636', + color: '#fff', + border: 'none', + borderRadius: 4, + padding: '8px 16px', + cursor: 'pointer', + fontWeight: 600, +}; +const secondaryBtn: React.CSSProperties = { + background: 'transparent', + color: '#c9d1d9', + border: '1px solid #30363d', + borderRadius: 4, + padding: '4px 12px', + cursor: 'pointer', +}; +const dangerBtn: React.CSSProperties = { + background: 'transparent', + color: '#ff7b72', + border: '1px solid #f85149', + borderRadius: 4, + padding: '4px 12px', + cursor: 'pointer', +}; diff --git a/src/web/src/pages/ProjectPrompts.tsx b/src/web/src/pages/ProjectPrompts.tsx new file mode 100644 index 0000000..24dbd5c --- /dev/null +++ b/src/web/src/pages/ProjectPrompts.tsx @@ -0,0 +1,177 @@ +import * as React from 'react'; +import { useState } from 'react'; +import { useParams, Link } from 'react-router-dom'; +import { api, type Prompt } from '../api'; +import { useFetch } from '../hooks/useFetch'; +import { PromptEditor, type PromptDraft } from '../components/PromptEditor'; + +/** + * Project prompts editor: + * - GET /api/v1/prompts?project= to list (project-scoped + globals) + * - filter to project-only for this view + * - inline create / edit / delete; Monaco for content + */ +export function ProjectPromptsPage(): React.JSX.Element { + const { name } = useParams<{ name: string }>(); + const projectName = name ?? ''; + const { data, error, loading, refetch } = useFetch( + () => api.get(`/api/v1/prompts?project=${encodeURIComponent(projectName)}`), + [projectName], + ); + + const [editingId, setEditingId] = useState(null); + const [creating, setCreating] = useState(false); + const [busy, setBusy] = useState(false); + + if (loading) return
Loading prompts…
; + if (error !== null) return
Error: {error.message}
; + const all = data ?? []; + // Filter to project-scoped only — the API includes globals as well. + const prompts = all.filter((p) => p.projectId !== null); + + async function handleCreate(draft: PromptDraft): Promise { + setBusy(true); + try { + await api.post('/api/v1/prompts', { + name: draft.name, + content: draft.content, + priority: draft.priority, + project: projectName, + }); + setCreating(false); + refetch(); + } finally { + setBusy(false); + } + } + + async function handleUpdate(id: string, draft: PromptDraft): Promise { + setBusy(true); + try { + await api.put(`/api/v1/prompts/${id}`, { + content: draft.content, + priority: draft.priority, + }); + setEditingId(null); + refetch(); + } finally { + setBusy(false); + } + } + + async function handleDelete(id: string, name: string): Promise { + if (!confirm(`Delete prompt '${name}'?`)) return; + await api.delete(`/api/v1/prompts/${id}`); + refetch(); + } + + return ( +
+

+ ← Projects +

+

{projectName} · prompts

+ {!creating && ( + + )} + {creating && ( +
+

New prompt

+ setCreating(false)} + busy={busy} + /> +
+ )} + {prompts.length === 0 && !creating && ( +

No prompts in this project yet.

+ )} +
    + {prompts.sort((a, b) => b.priority - a.priority).map((p) => ( +
  • + {editingId === p.id ? ( + <> +

    Edit prompt: {p.name}

    + handleUpdate(p.id, d)} + onCancel={() => setEditingId(null)} + busy={busy} + /> + + ) : ( + <> +
    +
    + {p.name} + + priority {p.priority} + +
    +
    + + +
    +
    +
    {p.content}
    + + )} +
  • + ))} +
+
+ ); +} + +const card: React.CSSProperties = { + background: '#161b22', + border: '1px solid #30363d', + borderRadius: 6, + padding: 16, + margin: '12px 0', +}; +const cardHeader: React.CSSProperties = { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 8, +}; +const contentPre: React.CSSProperties = { + background: '#0d1117', + color: '#c9d1d9', + padding: 12, + borderRadius: 4, + margin: 0, + whiteSpace: 'pre-wrap', + fontFamily: 'ui-monospace, "SF Mono", Menlo, monospace', + fontSize: 12, +}; +const primaryBtn: React.CSSProperties = { + background: '#238636', + color: '#fff', + border: 'none', + borderRadius: 4, + padding: '8px 16px', + cursor: 'pointer', + fontWeight: 600, +}; +const secondaryBtn: React.CSSProperties = { + background: 'transparent', + color: '#c9d1d9', + border: '1px solid #30363d', + borderRadius: 4, + padding: '4px 12px', + cursor: 'pointer', +}; +const dangerBtn: React.CSSProperties = { + background: 'transparent', + color: '#ff7b72', + border: '1px solid #f85149', + borderRadius: 4, + padding: '4px 12px', + cursor: 'pointer', +}; diff --git a/src/web/src/pages/Projects.tsx b/src/web/src/pages/Projects.tsx new file mode 100644 index 0000000..9a3dc82 --- /dev/null +++ b/src/web/src/pages/Projects.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; +import { Link } from 'react-router-dom'; +import { api, type Project } from '../api'; +import { useFetch } from '../hooks/useFetch'; + +export function ProjectsPage(): React.JSX.Element { + const { data, error, loading } = useFetch(() => api.get('/api/v1/projects')); + + if (loading) return
Loading projects…
; + if (error !== null) return
Error: {error.message}
; + const projects = data ?? []; + + return ( +
+

Projects

+

Pick a project to edit its prompts.

+ {projects.length === 0 &&

No projects yet.

} +
    + {projects.map((p) => ( +
  • + + {p.name} + + {p.description !== undefined && p.description !== '' && ( + {p.description} + )} +
  • + ))} +
+
+ ); +} diff --git a/src/web/tests/api.test.ts b/src/web/tests/api.test.ts new file mode 100644 index 0000000..212b50c --- /dev/null +++ b/src/web/tests/api.test.ts @@ -0,0 +1,62 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { api, setToken, clearToken, type ApiError } from '../src/api'; + +describe('api wrapper', () => { + beforeEach(() => { + clearToken(); + vi.restoreAllMocks(); + }); + + it('attaches the bearer token from localStorage', async () => { + setToken('mcpctl_pat_xyz'); + const fetchMock = vi.fn(async () => new Response(JSON.stringify({ ok: true }), { + status: 200, headers: { 'content-type': 'application/json' }, + })); + vi.stubGlobal('fetch', fetchMock); + + await api.get('/api/v1/agents'); + + const calls = fetchMock.mock.calls as unknown as Array<[unknown, RequestInit]>; + expect(calls.length).toBeGreaterThan(0); + const init = calls[0]![1]; + expect(init.headers).toMatchObject({ + 'Authorization': 'Bearer mcpctl_pat_xyz', + 'Content-Type': 'application/json', + }); + }); + + it('omits the bearer header when no token is set', async () => { + const fetchMock = vi.fn(async () => new Response('[]', { + status: 200, headers: { 'content-type': 'application/json' }, + })); + vi.stubGlobal('fetch', fetchMock); + + await api.get('/api/v1/agents'); + + const calls = fetchMock.mock.calls as unknown as Array<[unknown, RequestInit]>; + expect(calls.length).toBeGreaterThan(0); + const init = calls[0]![1]; + expect(init.headers).not.toHaveProperty('Authorization'); + }); + + it('throws an ApiError with status + parsed body on 4xx/5xx', async () => { + const fetchMock = vi.fn(async () => new Response( + JSON.stringify({ error: 'nope' }), + { status: 422, statusText: 'Unprocessable', headers: { 'content-type': 'application/json' } }, + )); + vi.stubGlobal('fetch', fetchMock); + + await expect(api.get('/api/v1/oops')).rejects.toMatchObject({ + status: 422, + body: { error: 'nope' }, + } satisfies Partial); + }); + + it('handles 204 No Content responses without parsing JSON', async () => { + const fetchMock = vi.fn(async () => new Response(null, { status: 204 })); + vi.stubGlobal('fetch', fetchMock); + + const result = await api.delete('/api/v1/prompts/abc'); + expect(result).toBeUndefined(); + }); +}); diff --git a/src/web/tests/login.test.tsx b/src/web/tests/login.test.tsx new file mode 100644 index 0000000..8616a41 --- /dev/null +++ b/src/web/tests/login.test.tsx @@ -0,0 +1,37 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { Login } from '../src/components/Login'; +import { getToken, clearToken } from '../src/api'; + +describe('Login', () => { + beforeEach(() => { + clearToken(); + }); + + it('stores the pasted token and calls onLogin', () => { + let logged = false; + render( { logged = true; }} />); + + const input = screen.getByPlaceholderText(/mcpctl_pat_/i) as HTMLInputElement; + fireEvent.change(input, { target: { value: 'mcpctl_pat_test_abc' } }); + fireEvent.click(screen.getByText(/Continue/)); + + expect(getToken()).toBe('mcpctl_pat_test_abc'); + expect(logged).toBe(true); + }); + + it('does nothing on empty submit', () => { + let logged = false; + render( { logged = true; }} />); + fireEvent.click(screen.getByText(/Continue/)); + expect(getToken()).toBeNull(); + expect(logged).toBe(false); + }); + + it('toggles the help panel', () => { + render( {}} />); + expect(screen.queryByText(/mcpctl auth login/)).toBeNull(); + fireEvent.click(screen.getByText(/Where do I get a token/)); + expect(screen.getByText(/mcpctl auth login/)).toBeInTheDocument(); + }); +}); diff --git a/src/web/tests/setup.ts b/src/web/tests/setup.ts new file mode 100644 index 0000000..7b0828b --- /dev/null +++ b/src/web/tests/setup.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom'; diff --git a/src/web/tsconfig.json b/src/web/tsconfig.json new file mode 100644 index 0000000..708feb3 --- /dev/null +++ b/src/web/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "Bundler", + "jsx": "react-jsx", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "resolveJsonModule": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "exactOptionalPropertyTypes": true, + "noUncheckedIndexedAccess": true, + "allowImportingTsExtensions": false, + "verbatimModuleSyntax": false, + "noEmit": true + }, + "include": ["src/**/*", "tests/**/*"] +} diff --git a/src/web/vite.config.ts b/src/web/vite.config.ts new file mode 100644 index 0000000..9de9b3e --- /dev/null +++ b/src/web/vite.config.ts @@ -0,0 +1,41 @@ +/// +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +/** + * Vite config for the @mcpctl/web prompt editor. + * + * - `base: '/ui/'` so production builds work when served by mcpd at + * `https://mcpctl.ad.itaz.eu/ui/` via @fastify/static. + * - Dev server proxies `/api` to mcpd so the same fetch wrapper works in + * both modes (in prod the UI is same-origin with mcpd, so no proxy needed). + * Override the dev target via `MCPCTL_API_URL` for non-default deployments. + * - The build artifact lands in `dist/` and is consumed by + * `scripts/build-rpm.sh` in Stage 6. + */ +const apiTarget = process.env['MCPCTL_API_URL'] ?? 'https://mcpctl.ad.itaz.eu'; + +export default defineConfig({ + plugins: [react()], + base: '/ui/', + server: { + port: 5173, + proxy: { + '/api': { + target: apiTarget, + changeOrigin: true, + secure: false, + }, + }, + }, + build: { + outDir: 'dist', + sourcemap: true, + chunkSizeWarningLimit: 2500, + }, + test: { + environment: 'jsdom', + globals: true, + setupFiles: ['./tests/setup.ts'], + }, +});