From 0d03be47a09f4066adde4eea706eb72ab2200bb1 Mon Sep 17 00:00:00 2001 From: jerick Date: Sun, 22 Feb 2026 23:18:57 -0500 Subject: [PATCH] Initial commit, prod ready --- .claude/settings.local.json | 8 + README.md | 79 + backend/.env | 3 + backend/.env.example | 5 + backend/__pycache__/config.cpython-311.pyc | Bin 0 -> 777 bytes backend/__pycache__/library.cpython-311.pyc | Bin 0 -> 4917 bytes backend/__pycache__/main.cpython-311.pyc | Bin 0 -> 6727 bytes backend/__pycache__/search.cpython-311.pyc | Bin 0 -> 2547 bytes backend/__pycache__/tagger.cpython-311.pyc | Bin 0 -> 3534 bytes backend/__pycache__/youtube.cpython-311.pyc | Bin 0 -> 9403 bytes backend/config.py | 13 + backend/library.py | 108 + backend/main.py | 133 + backend/requirements.txt | 8 + backend/search.py | 45 + backend/tagger.py | 65 + backend/youtube.py | 207 ++ deploy.sh | 173 + frontend/.gitignore | 24 + frontend/README.md | 73 + frontend/eslint.config.js | 23 + frontend/index.html | 13 + frontend/package-lock.json | 3308 +++++++++++++++++++ frontend/package.json | 31 + frontend/public/vite.svg | 1 + frontend/src/App.css | 1 + frontend/src/App.tsx | 79 + frontend/src/api/client.ts | 51 + frontend/src/api/types.ts | 54 + frontend/src/assets/react.svg | 1 + frontend/src/components/Library.tsx | 199 ++ frontend/src/components/MusicSearch.tsx | 92 + frontend/src/components/YouTubePanel.tsx | 187 ++ frontend/src/index.css | 475 +++ frontend/src/main.tsx | 10 + frontend/tsconfig.app.json | 28 + frontend/tsconfig.json | 7 + frontend/tsconfig.node.json | 26 + frontend/vite.config.ts | 15 + music-orchestrator.service | 35 + nginx.conf | 43 + 41 files changed, 5623 insertions(+) create mode 100644 .claude/settings.local.json create mode 100644 README.md create mode 100644 backend/.env create mode 100644 backend/.env.example create mode 100644 backend/__pycache__/config.cpython-311.pyc create mode 100644 backend/__pycache__/library.cpython-311.pyc create mode 100644 backend/__pycache__/main.cpython-311.pyc create mode 100644 backend/__pycache__/search.cpython-311.pyc create mode 100644 backend/__pycache__/tagger.cpython-311.pyc create mode 100644 backend/__pycache__/youtube.cpython-311.pyc create mode 100644 backend/config.py create mode 100644 backend/library.py create mode 100644 backend/main.py create mode 100644 backend/requirements.txt create mode 100644 backend/search.py create mode 100644 backend/tagger.py create mode 100644 backend/youtube.py create mode 100644 deploy.sh create mode 100644 frontend/.gitignore create mode 100644 frontend/README.md create mode 100644 frontend/eslint.config.js create mode 100644 frontend/index.html create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/public/vite.svg create mode 100644 frontend/src/App.css create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/api/client.ts create mode 100644 frontend/src/api/types.ts create mode 100644 frontend/src/assets/react.svg create mode 100644 frontend/src/components/Library.tsx create mode 100644 frontend/src/components/MusicSearch.tsx create mode 100644 frontend/src/components/YouTubePanel.tsx create mode 100644 frontend/src/index.css create mode 100644 frontend/src/main.tsx create mode 100644 frontend/tsconfig.app.json create mode 100644 frontend/tsconfig.json create mode 100644 frontend/tsconfig.node.json create mode 100644 frontend/vite.config.ts create mode 100644 music-orchestrator.service create mode 100644 nginx.conf diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..a8e58c1 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,8 @@ +{ + "permissions": { + "allow": [ + "Bash(npm install:*)", + "Bash(python -m py_compile:*)" + ] + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..aa4107c --- /dev/null +++ b/README.md @@ -0,0 +1,79 @@ +# Music Orchestrator + +Search → preview on YouTube → download as tagged MP3, organized for PlexAmp. + +## Production deploy (Ubuntu server, local network) + +Copy the project to your Ubuntu machine, then: + +```bash +bash deploy.sh +``` + +The script installs all dependencies, builds the frontend, configures nginx and +a systemd service, and starts everything. Re-run it at any time to apply updates. + +**Access:** `http://` from any device on your local network. + +### After deploy + +```bash +# Stream live logs +journalctl -u music-orchestrator@ -f + +# Restart the API +sudo systemctl restart music-orchestrator@ + +# Change music library path +nano /opt/music-orchestrator/backend/.env +sudo systemctl restart music-orchestrator@ +``` + +--- + +## Local development + +**Requirements:** Python 3.11+, Node 18+, ffmpeg + +```bash +# Backend +cd backend +python -m venv venv +source venv/bin/activate +pip install -r requirements.txt +cp .env.example .env # set MUSIC_DIR +uvicorn main:app --reload --port 8000 +``` + +```bash +# Frontend (separate terminal) +cd frontend +npm install +npm run dev +``` + +Open `http://localhost:5173` — Vite proxies `/api` to the backend automatically. + +--- + +## Music library structure + +Downloads are saved as: + +``` +Music/ +└── Queen/ + └── A Night at the Opera/ + └── 01 - Bohemian Rhapsody.mp3 +``` + +Set `MUSIC_DIR` in `backend/.env` to your Plex library folder. + +## Architecture notes + +- **1 uvicorn worker only** — download job state is held in memory. Multiple + workers would each have their own job store, breaking status polling. +- nginx serves the React build as static files and reverse-proxies `/api/*` + to the FastAPI backend on `127.0.0.1:8000`. +- Metadata (title, artist, album, artwork) comes from the iTunes Search API. + The YouTube video is only used for audio. diff --git a/backend/.env b/backend/.env new file mode 100644 index 0000000..4ba901a --- /dev/null +++ b/backend/.env @@ -0,0 +1,3 @@ +# Path to your Plex music library folder +# Change this to wherever Plex reads music from on your server +MUSIC_DIR=./Music diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..b0831ed --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,5 @@ +# Path to your Plex music library folder +MUSIC_DIR=/mnt/media/Music + +# Optional: restrict CORS to your frontend URL in production +# FRONTEND_URL=http://your-server-ip:5173 diff --git a/backend/__pycache__/config.cpython-311.pyc b/backend/__pycache__/config.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5e4eb1f8b169a572e355390f276874d9dfac8af8 GIT binary patch literal 777 zcmbVJy-(Xf6o2P9{)p{-FqNfyN@Pn=386rj(n<`eP@zkfDCDlejU7j}L2V}&Rf5q2 z5|LOurBFKeKjLg@T!O4>mPmb`x6Kni5ORQr0CSl8#^?F}+J%6{JnBEX5Q+FNU0}2W_0sR8 z(^qvAv)f&FpL)r#y10WamkbYMvGrzW>v?B$>-9%o)mjIR-rKD+F+b|sWav7vK;G+) z+pz|*u=}F5odS!h=9;5=DP}AuLAv{fzGoL`nV6f?+H4y1Nm_%qiDMau#7_PsE}8?Z z+jjDrmvBdCxUcPXdjm4WTF)B3)%)t`!@aO>TKK>q&+G+?NIuK?L8sdSLTCn}myQ`c zn&x!|kEeP436dv$Qa&r5a?62IJK6LX{e{!Taq~h9Ys*n>Ih0o-dF4iKUdzo;-iYK4 zPoyP|D@THYiTu<)*@-m a6J{VBmygQ68(^;-!cqiF(@gCuy~!WC-lr)5 literal 0 HcmV?d00001 diff --git a/backend/__pycache__/library.cpython-311.pyc b/backend/__pycache__/library.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..eecf1977bb86b673b0fe76d899e879b23f0510a7 GIT binary patch literal 4917 zcmd^C-ESMm5#J+^A0kCk5+zznBzZ$gmT60r9m$_*?7DH}%84b{P8wK+OPqKo>1^^y z>>Xvxp%66)P+1jfAxL30aS*m}5(jC4qAz{vL;eJwaDdnY1PCqikT(WyfL2eP-6JWU zY!z-^d)%A-oY~pEncoaQ4+MOOK7aX0o^M3xAFNX$u1fv**U-3+L^O#+PIRQWNe*gf z%8_dZ|DifW1Puf7PMKYo#&K!s2VQ4 zd3EfS#QCu+rkhAwjws36T|lD&UMIYdw;c42XV3!DxSa(zYVFTp@XOkS>d2$3NUMG! z7`JgziR`g+eFoCLtVukNR=MjV3tXNn@YVJ8wVhYxR@y;3NBy3_S=(uE&fU9S|J#By z?|jBR_radLZBOU-+VhvZBp=L5$R{AS(({Adq;u8!X!@=x-wax2HFy~bR^0+^ohP&%t~olQ1F#mA**J@`FJ`z938;XEWkuf-=U}NJM>=!;Dy=&>CV^ddOI`KQESBN zouR(I&BxOt0;SnupqZJOZC89cB_v}`(_1})$x{_gUX|}iF^B15cVxP))5Cz9ICfO`p}El1U5Qzk#X4`c1U&V+tqe zsZ1yArBY|6cLvXP%M^S{*l?&4LICAW|BR$1Dt*!iiv#eg%V4;Rs2oLo{figA_O}}T z=XHOV;qNLV&b6-;iYx`cj&y!}&FG5jT}Oq zJrp%UQDALoD>b%m^~aXnn;rWLz7721I(|{dBL*JPJB}G0$I7Uw;Wf^pOJk*Q=SH|^ zJ=~**`;2fO2nckPn%cM8_AQNWcJ~&pZS);q?>nydoizGR>fNV|?o(yt4TKq4^_9Y1 zM!0t)d}uvYrisKB|Z5Jh}s5TBu)51+m8ts0*Ae z=x}yOe0m6~=cwP+&TH?fA*~h=5TsJ}*>`Dht_7{CrnI(etF~y=NGp#B$-zhH<6SV$ zWIZ*I*=3P7t+^@*p*m>ar5lHoR?zMlLKug%eLE8K+)l8vpB7R^Cj<%p&MHH*^H#nl zu#637NF^nsF`^0?Sj*sAeidh2R|Hv=RQyvRm6Kj21lZ(h2IWLjGpRWUCe&cCq$Y(d ztgCV3gKQkYVh&PBA%$mh0uf`D(Czlilxb&DgiJCC=oa7hk7qQ$QY#}n8GAN$KC2|PLorRG^T0#YPxeuN>*-~ zZ3!Z!Gk}&-I;+it^%F^uE-6v$M)i+`W&SM3BY zs?`&Z-3d7aZc=mn1x|5@+*KHRitf)no9=)5Ih9cV#vFIXSQeJcR+27iS2Zt_!8^2B z$b-Pbu#&ljnZXS}E|$q=>7ON-s(DX*JmzNM-!fjOpv;ptIHTzzX&RYFpdE9NlXS?F z%n@d^b;e1CVTO^nprI}USwvgGeTx@LzUJRtTlaMrecgoB7>5t;Xi1SId6nZ`ng2 z$b4#HCT6p%T8Odis{X6|zlRtAaAyU;;mY@iFqtL zJxm7%C>f+AP6-8MGDOKSAU5!9sUUiSDQaQp8CWz$7v2K0H->KdgZ~?XmYxkkVcFs( zW}C5)=}9ESOfr!mM?e?TL+^~HuoT`PWuK=GODOJ`?qo)pmS?P7olilkZt|j>)U3Pc zgC#6BSPn@~_yuBnuo$xK{Xse-=2FrbG73`^GSzQ@l${*Ml^srw2P_BTLtJn%Kz}9F zUbL4IiWKc-3w0LlrR+Y!g;(EL>s?a|udLmE5I5RlWdvip$u!r>#nyVt2&zIat9FyP gmuo4!0SPeI@Rci2wiq literal 0 HcmV?d00001 diff --git a/backend/__pycache__/main.cpython-311.pyc b/backend/__pycache__/main.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8ba7d0dc0fbc98d78adc9325aba924f0c3827edf GIT binary patch literal 6727 zcmdTIOKcm*b#|AZtGH6<`%f3Z)2K1@XZJQb30Sb&4E(Bmo|X1q>MJr3QMS;iL$1>YLeJ zexxcjilRV=%eQZ4-n{v|@9gheTHFZAugmYJjs+0e!4;^53xEhMrGrz44B1=WhQm&mGt@cPGr< zRa;5}A_bf0mO`>0c1QLWXxtm@sN;rh(~i2g-BITzu3hxZwwxGn?cTz*XIrj4o4Ecv zDX`LxE!=ki_uhsBsG-s(?uG_5&j`}*t?gv<`Sfi$^}cO)(%;PYJLlrzly+|6-7h&} zC@kcLU<2+`IW?6OPUGa9tZG=)6fAsMSP_njsy2G&xG*gy-;}eG`w`h}n9;e>>_V8+ zoo2t@GC4JMX5vaxp4U=JR=1zc$#?By6a_8l}{;>oQ~$(x6TPH z?OIAp%Q`P9Ni~tfY2AfozzLeHoirepwLZB7m0L(gaR$5udmhOg*fsMKv{H<%VO$Nf z5ljNl0Tyb{JAu+BIcS?R?xdr5v&Pe!%dEL*&25Z^v-9Y! z2LZz&0y+SEf}Q|G-?o~P9nOfUY-D}`w-5>fh_S00fI`Es($ZEqSvdLj$x3T`AzFyO z9j&wl3a1OF-#%S$=^rS(vK&~}e$`X74Zu^f4OH6N3*$?>%_06kVPfe8b67`L;lwgm zv~|H#q7V?=Z!IJ59~S3R!z!Fja&9<7)HL#lI9bppcP5ohq~+|aHm5TebqAKU9L^?d zE*PjB@P7yXPnH0@f*4R07t$IiF4XWv)NE4kK$yA8y^3ye@7iK$H_~jt!YqRbcVA;P zP#vpCti&xc@1qadJGN$rf1Qz-Ys_2BW#%%v!o7wrGhusPh|vuRsi|C6R)ra`H&w~b z3K>}wB~cS2(MSJ)mE}830%QNej0pB59TGlab&m>C&Z&tcSd;FMU|cF4w&`AaLIFqV`%3a+O0Gc-C=G+`=_-&@C~fx8?Hezy2zv$2ne?PFWmx^mC z1s#tOfUgLsBA0J_P&ir*qL$$Dt3^+5(bh{NPrk27F^8`ji@i2%_*_gffF?yH%0!gz z$zK56dddKEjag(0eyygIkdr_V*&03;8OdJ189>d^SX-+pJ4{_GlCwFOEi&8C@y&FS z>jMaOVRwG=3@Bq=bKw94WAk;@y=u1X6Xvlp3ocCEC(J;6A_>VpLB<$@+BFDdDtszH&EDC>h4nP5Y?LeOPvRhc|D7lBq?xDh1#py0wHT~Ln z(Kk`@O_Y5TMSkL8duVxXWj|=#s(a-$ylx$P@7U^zQgBZ>xCd1AR_A-2tDPnPuCjku z)n)gB3L3!fFqF(ol`Nf_#v)!w!t}L5d>K`zAwafMr`yn1zZ`vr%%Ngl>Nz;1Z?-F1dP#Y?i!&* zrdhA=uv?Nc&Pd8M`V|6}0|nB$&0KB&8CNUSlkAI#h6YFeI`!=1a$43P1E7{7fQ8H` zX-URHDyzcHfr^bLYl3-Dz+WzqreA!S_ zgF7Y;hbq zRr0?Fz%sx<2jo}lgQdW5IWWA$FY*5)`-6Dv?RwDCw%qlL*KfXF3GA%+yI1-u{%4Cr zXYX}=(FZT{X*)-Q=wlK@zp6*lbFhi1nICZ&>^agQdKEQo{(1X-Hjl7xeXPjYD|&X;;bDwR!<_FMiX<6F;RJPU)s zogkHmeSz;{m@3OMwkjfkM@(lCbu>N?(ShRjPX+BQHa`{AUu=FJqQ0W_RFJPge-#ue zT2IxBoW83Hx~pg_SWZ1AtzXr>dFS=cqN}&W z^p%;uitx;ueJy!cEDA4{gcr-gi;y%i({;UTX}HAnl$o9?+sgEnS{+hGq|G!Z{U_r!OGN+x literal 0 HcmV?d00001 diff --git a/backend/__pycache__/search.cpython-311.pyc b/backend/__pycache__/search.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d5bef71f01a18e0e56b5e2a815f3499260a68a46 GIT binary patch literal 2547 zcma)7&2JM&6rc5ec)d*8%h*Vjd-AS^r{^;yF zAJi6+(pD)#YL!c(6e+$yQ^CPfrHVrjh`%7NR_ba_k$NdNBdCNp^{owd5(v%g&Trn# z{^q?mZ)V>9(%2{=D8K#wvtHm4`rGZiDU~nKd43sS32A5)X^iGEm{A5=*6<|RQIAm^F1(PGU>Z6ibm! ztPpilp3)WBn#8hxIh(?!e5pD*^!fQ-=`6vxM^2?>C2LLg$OKzik}_qBDC!MaPmQPL zL$YFwWs|Z(Ecwb8pP9YVQ`eJ`Zicc$so9(RCM_#tp6u(>EmwQ5lF1mjS4}7Ts@9*n zE2sR|SxjarZ|F(gqC(Y<>Zz>>p&-cfP@fYPp$mwCAmu6{%0YPUa(R!%)Y;c-vc^5+ z??dlFZR7}=XEgK;`z2apzvuF7jxB7qQ&VCIH4)`J4~6@{dQfA|!xq(aa5=8P)!B9F zK8O!$T?PRUHva9hm6zu=(5|6v7Cm)~3`#Y3qQhu@)0oLkduG^cXqsWrHHZ&`CQx3; z@$X)p=AGE4{|x$x`AW)rbKV_hh?;M!M|m+Pl1NT0Y>HHUZE#HeZ}EkKx6Y_bJKAj6 zx_3MB8n^qlBeQAO%)qXh!CiXUyh~nayJa{HE#Gar;ybQx&-R%)aYt@=J6DU`m9~1w zz?rzykP{$%w2n|d>820Lnd#I-H%B=OlOzmcO;@OZQfbei0_LX5-x{S;9jG&4}g#;ETYh)_ZFRx-_bjruCWh?2xq zf~mS`Rpcf$Z5UX!^mMAy_g87zjAG%Zu75O`A$n3Fv*&P% z;Hu&b!BaY(2A{em5_VS-aiP_PHW%E~N@6a=0VqG=&aF7~dM%_K#=AwzyIF6B`iIQf zl=`uuLv$!Rfh{VXzkFrnlS>Iml{82c-NcFUG)b72Vr5OrU5D7ZLCP44im3p}5~s9o zA91rrHMRlQu%K2|iV)6BI%Q(Yxe1-PNs@B9g_G0^QA%WqVftW9ep6mGk3r@Jov*WR zZ)TP@R1W&-z1a~z}(q?n@})p2jsQT zdZ4Ei=&=JmWyJFP-Va5=iSApCt;FtqTAW&rm7=HY=&ALV?eBu?xftvk0CxUzSPsb{X`7yobm{m#Ar>&urf-${^Apn2|`6Wp^TE{e<1 z!f+{gzz!ZLqh{X!%!LJRBNSQcTI^ary*6A5_1U4m1+U`|l@a6nh;br&m(q*r!j0mG zrO0VJa(dz1h8Xy<>2{OT8ef`OoGJK9tq1MagHCksYTHU%;m{f>MF;HYfD>z9?ON$7 z3>HJB*bzH+#A)BZI=M2re&G0{P^o>;ZXa~w`&Lh^oG4r@#Ru$oeMTwPZ^!y;UyB2! z_%S;`ef}3NK-UM$ zcBL3=TbP+~9o@U;w+BYNC*+H<8fNY9c{8fJg;vHlwE|-2Xfx zAGjl|X%WimaECYpZay4#U9~DF^;h&92#E9oyb=eKX)SBuQ{)76-52*Wf-5imY#aHyV5Aq_WV#w3HFRvRY}@Ix9`ldUWP-%VYI%+aDwL@e7r) K9f2GN)Bgd;HceCj literal 0 HcmV?d00001 diff --git a/backend/__pycache__/tagger.cpython-311.pyc b/backend/__pycache__/tagger.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d453bf53f18a4ee33938d3c2e810d93f8839d761 GIT binary patch literal 3534 zcmcgu&2JmW6`%bgm&@gcMA4EX#acH>L%NCLNUnj@jtzyfQChCz+HP7CXo(ePBva;+ z!tBtJTpD#Fqm_dMkpmZ2(Lmm-|QxPC1_b(s|0wop@5TKVDy=>qV5OV69rAUbq zt@hL*=k42hZ-4V?2|9hps{o zkO&rUz|2^KyC8HOdI*a%y*PYZ&O~v9-;Yv4NCn7ty~5v8Myk*9Fn_Ls~MC~{~yY{`yd;xDhKTWatHcDkziVbw9Aq0nQmtekG1_#rMLHfCZ7~qf;1TMBAfECpNs5k2in$U7K67M778&KKOnGio z&gPV#=qbuGh4Llb(DD|L-WzlCWY$qqrR#(y6*>SGN4D}nFEU%22Z@QD0|^ugjHX>5 z&4QzyFBOv^$}`)MY(^>HC&!0L!7LQKn7U2w&FzG>mHR)JADdsyRTJr2B3)0U*ZS6Gem~TZ z)oyOrs)=*8#JPIn+`exABO0zIQnf^?o=EMBMyiR4T4JJ}m}t6v<&RbU!wpscIQT(u z^=wTYtgC}nb#SA9aQ(-hUwS-Ld;L`X^;6aU)3yH7_5Rba0q@<>Vd-kjMVb=cIQZ(r zVC~?s`oUw>*zW&|+6aQ18VEP^=*N>EOs@HA`fyzzuIj@ZgTsx3FErwZ8}S2;_)sJM zq8s!Lg}+81zTskJV)ekC^KO)Ha{B%BN4eF+9qU8up>Qi*laJKpBh}X5=4bo%&|&oH zz)9&#;iTk9kSMu!i2^InnPuqD24J*XiJNPCW8sn5Nf^h_vVc*WY=nxi-YzqM*>1Z_ zBT&@#Q&3R4m$uGwD4>cs*Djwq5d#KBom8?6qpfHJbP2tL-b6dcY zihX=LJw>0=w-Fnt$6kJTu^KyCiyf`Uj=D$+ytu!3e7135=>FU%bL+9YrF*4n^p#!z zmGOoi`n!JUZ~CE{Zq#+7svC`1-@Uh7|6cH~`Gnv6_gVLkX@4uHl6_~`{I`m9kB#~` zB{fAO5?f&_jFV9xAlS!X|85VCpFn>+KAw=C#Ct(~lF-IS{ZB?Eu76+P`f*P`p}iB3 z{;CQflQGAi&Eirno29P-hU4cSorU}~N6A5-u0WDS86Ncoy1kfrNDlf-6db40{7YLqh15 z&VLOwQ0@G4H<7#je}tYCzSju%x)NN!ipE?8u0V*rao5i^74`JF0j_B%c3@4ud&CX$ zI~@hXZis7Pq(|Hc*P{HX9$POdj;*+U1hF;?F2xy1v?!99 z8CsT>+;TT*S8=^C6WB1jwow*XxY~6Vs}?BgzJL^2Y=VA~27wS$7%*A{X#N;D28?|5 zoEefL_1Miv@64Qg?(5vgx%ZrN&-|U;ZY2;VAKwfAn~#wH!9+E9D#-K46d`xXWg?IX zB2a=MLQPOwY?v@;F+D*;Oh=eGc7mm_4ihoXnI=pcZitvC%#g=MEOXWg>l`=1Q6x+z zY@%IsXrF1y&|2-4)Z?O4Fdl&tp-V7}4#6Te30BdyzD-?gqrkn-PPn!D(8G7AZ-1Ye zXcinm+puVYzO15SiV>WU>k*ouR~LM4_?qGK)M|MlwFSNw_^^dr@Iec&&*uYH7)HAdYPAZTm9a1a59v0+aFO`8 zFD_fJ#1fYhlOivRK`AuD2crTXObFo^FU01fkyudRuZ4sBg0e@5#0RVw^;!v8lq-2V zWPUOcjwpM=QOK!K*%^)T$IqYUXT=3RDvE+A_y?>b6@oG^URR`GNU3xbl=*Y#_wfli z9G&K;rsm?}G#`)2%AU9s3yCsj4_Lp#JuvR4ROWn8nSq!-9$oMoRLi-`7f+uEjGVrp znlFu>JFj7E;A(7A{)&YrD*J9ilonKOE_gj4iE<*M$f{8im4p-x8IWit7$JTSzULVL zZx9N0HSA0MG2CZT27yje_lUqG$vyIbNzn@I{A##b!yiJ{qiQxuF-ay@-%G7TU={4K z5|VV%@Q{I)@af?_BCtQE-y(PEzhr<@#>JBtv@!EaEY6SfRjyTL#81T}KFDiR<>$l7 zOe~?)7>LUnj4miMxR96p3@%AnR8%e+3(Na zZW0O=peRHFu{iL;rk4hEHZe#>M#k|NzI9IqL$hL37+ioE!8RR;FQ|@6i%OQ%4z*k0 zlivmKzwo3<*+|@5N-garUq`8RYuUm!8OsDfdaP_Cp8eU`eDgkdmq+zDefBBmE^=K3 zuDi%}r$;Zd%j4mZpOIsf0~iSsLj#rUYx!1oD7;Knpg+ z9vB4sePd%Cirl=PWa0ce#!rIGGS^O&366{u7G>2C7E};YN?3`AsyPuyi7QH~DKry| zM#Ts;Nk~B@9E&ct%qU7+K0G)W9#~MqbJGK%*xcZ?@Zh3jaPD0}oC<vpbvm*Q)Uu#lhI%}a#@1TwHAOu z3nLt4Pywn1L=T?Ck+Z8ZBOINIsWiv})fkM&;kc?z0gy;@L1LL%jwS#uUzu<$r-3E5&UPNAU>W)wWk zeWt@Q&Br>4UxfGn9 zMw1Fm9hnbDWe}viG6M!QEOb3(B`DAG!6`+QwBlMvAc^Anq<|^{T6MjNXM}9_YfRM@O=a)5R4!g1)v)Bxy!h?xO`gBL@*d&3T&DhQRxCy z!8YMW2lf@%WL~t%yvDy6UF!Sk zxfc$?+KNoeQ!~g#cV;X*l@pgIN;Y4%C&%V~{54Bktltm-U(+mW{DuI4)&Ok8=1!l} zkHMco|DoD$fCSo_+8(|Mhqq?blL$#!1cOrBwFHFK>NQcIwcS|Tflz8_+*v8BQeR51 zDdieUnKh-hBr6${tR|$`2Zksm7*qD7{T@l$(1=Jm8cv3F5e$_ihcM+#IyW|^1e0L; z*xV=>DN=9ZC^lG$O27+@V0mCbnrp_^lyoK?P-@jnQ!b<|aLFdYCfEgs;Ji;yQ;M@n z3D>QW!Zal`-8ZJ(f(wN%%%)j!t=7I;<4vQSNO>$I>B(vChSl0SCJ+TxekCNElkRmF zB~ra8f#EyaO%!leR6`GuUQ2le_n+W(y)o!*oYAH&-MCAxfbpI3{=_Sgixq61nkTx^ zy>88`tGXq*C08fas$&Ii0Q#twl(+w>aSP3Vd~`S}=*v+fS%)NDjq?O|jAv77+KB)j zS#ou}UnQ1wfjma3tjXl0`D5=x&?Jv)+5|4M^*q|Ojz?R8M_ZC!+%cPup<%tFW_{gP zS6gV=w52wB!MAC7t9IV&_kb(cv^u+0to~#+u3lqm)1KO}C*+F1eOwdxDpwWFpi-nU zD3#EjSfu$qyk?_Z0;|Ln4@zJdgCY>GgMtslW*5x^y*uH??w1#tfw}m;MaQHFLK*!w zgNUi_lPA%SGO9aTj$?sJLq(Mu^3y5}ei_vfm&7U1C!!FF31UcXs-@pe1S4T(!Ecms zmrA&ORHJUst0u7i6;NQ%99%RN4^2#xk({os0yDALtSs%orf&d9yaM1vEP73p6j9(& zm+{e<0xk>GzyF=S&4=Zx+XvLC5{yJd;h1Uyb&Pt}Z&uAP@IV~gKAN`DEEZe_O(R-0 zV@Ja`Qe%s*s=BUtIu7$HBgCTOR|1ajG=eh#{4Ujq9m0B`A_c#N9Gnt$vMB0Rc5YS( zOVU|vfQGKx9GDBvih+h%Yx-A0->BrpIyUrvR2i&VMz4p&INs${qsBomXw?(CJ*eUOMbn794qsR8Rm&nEE8-kn zHev4sV4kWOlJNlWTD4Avqrr&2{ODy-X)qKe)RrnOUJq4j0*7QQt=I``BmL@j9x6x( zaP8$6pnfafnk|F{9gfelZM)GPc>i2xXMt(|(%q4LtKjY_x_i=ZJcY#Ng1fuu?oOY< z#6-cpv*_NLK2!1^$lH%*YXvy9fZ@^tmsczU=D-XWw#L!MQIz zR$^>FAOG2ScK>oudc4347Ma03Gx(*;bLZ{c!92fDe+%Ay1=s$fYdsrj1R+e^*u&zg6Xwsn{I?y`yLfUA`b+PN22(lNMvtu9NTmIf7`JlKOQSMMvIQoym{mIu> zm^ABL1*Pr&U$9G4H|?33g1Pf)S5NNZ-}-*#tK>}=%-c$4$C9!7z-{+s*#s0Ieqkd` zzU*7sL3|2MKdb`ReCybaV+F3G$aQFaG?FiMU0O@{8Laf1KYKHCroeO-na(`Z3G3Fm zvyRzigPnc75wxtQ-d3~$|F^X6^!ueJ^>w8Qlr=iv zcjk+p!%un+|NEgr&uFn{w7{P%@+a%+36r$mKeiOj|kArb~jXM%-0_|CA zS)vBp*ujV;zoy3hw6q&WCG{id1OOR00SVnd5-J24hYn&wmkhI*jo=@#XbHeZPMr>L z>X^A(wi~ugD08`BZZDeK^X7JR=)f+l0(9tfphKrFZ?$v{hHU+#Rdwe_OP%wh!RGXfhhz9L;sl3X>jAr{u$L%)f&6qNb$Y`ujiFeiYs&1k)lY z%QdzTE>R_XK-bMjyYu6I!Eeo6EtR>t1(Wif+WW2Jy7hDkx>7kM24+NHash7Hs;4dy z0R0vQx1^{WUrklrEDCz%uP#G#%__i4cQh=|z@IBx8o=Dw5ukAZj-fw?2%NUqi)vjZ zqN(eupD3+gmKijM7P~gE76y()VxeF}9v;9d{|Y4KzX$LV{YbW@w{O0l-SdRok>_^6 zrTOmYN2A$21<%f+XJ_7XcKP`4Mt?J!-#hksSE2uGvH$GSXsNaBZ@jL zpxAm~>5T4aw4bJ)_7ARDA73sEeIGrArwaXJ#s0B7Y-Z}VJ$tp#AQI@yQ^U9&1-L&CXO9Yrro{q&zukrDSm@)v)~z) zal`(oa>TC!E+Ig7l8l};m}uExVt3zRz~#v9&VtHl4ubb)i#4_s>Xw1x0^=PeywW#1+?c z>(GrunYVPUm}fe)k*W+Dcn#x4dr}+k3LtpQa}ci&4xJnU5-?L{pP{UU~X?tDRBKouD_bqm+j6%R!@=Z`GVW^gxj^ezrYO@xgq!g z$T{;|N9ItKi_otbDEr%0QveM5P#~b2`X6HF=K$axO__;=Cne+p+$9#`c%ehtrD;Od z2>$Y^@U(=?*DetHUi$!(c&SjGfdKsR0f~Ya1Q|L&d80BBxanwrIQ1VNXgX;AHqDOK zRHk;QwB4vJ!>@_%3&Wo!-PJe#!bMx>ipKJ86qla8B_rbWlCI08<`ZPhW@^|S*=;nZ;urp~fU$K5!R zW*zqsqqXeSak5b~X)sV7xV#YI@ list[dict[str, Any]]: + """ + Walk the Music directory and return a nested structure: + [ + { + "artist": "Queen", + "albums": [ + { + "album": "A Night at the Opera", + "tracks": [ + { "filename": "01 - Bohemian Rhapsody.mp3", "path": "Queen/A Night at the Opera/01 - Bohemian Rhapsody.mp3" } + ] + } + ] + } + ] + """ + artists: list[dict[str, Any]] = [] + + if not MUSIC_DIR.exists(): + return artists + + for artist_dir in sorted(MUSIC_DIR.iterdir()): + if not artist_dir.is_dir(): + continue + + albums: list[dict[str, Any]] = [] + for album_dir in sorted(artist_dir.iterdir()): + if not album_dir.is_dir(): + continue + + tracks = [] + for track_file in sorted(album_dir.iterdir()): + if track_file.suffix.lower() not in {".mp3", ".flac", ".m4a", ".ogg"}: + continue + rel = track_file.relative_to(MUSIC_DIR) + tracks.append({ + "filename": track_file.name, + "path": rel.as_posix(), + "size": track_file.stat().st_size, + }) + + if tracks: + albums.append({"album": album_dir.name, "tracks": tracks}) + + if albums: + artists.append({"artist": artist_dir.name, "albums": albums}) + + return artists + + +def delete_path(rel_path: str) -> None: + """ + Delete a file or directory relative to MUSIC_DIR. + Raises ValueError if the resolved path escapes MUSIC_DIR (path traversal guard). + """ + target = (MUSIC_DIR / rel_path).resolve() + + # Security: ensure target is inside MUSIC_DIR + if not str(target).startswith(str(MUSIC_DIR)): + raise ValueError("Path traversal detected.") + + if not target.exists(): + raise FileNotFoundError(f"Not found: {rel_path}") + + if target.is_dir(): + shutil.rmtree(target) + else: + target.unlink() + + # Clean up empty parent directories (album → artist) + _remove_empty_parents(target.parent) + + +def _remove_empty_parents(directory: Path) -> None: + """Walk up and remove directories that are now empty, stopping at MUSIC_DIR.""" + current = directory + while current != MUSIC_DIR and current.is_dir(): + if not any(current.iterdir()): + current.rmdir() + current = current.parent + else: + break + + +def resolve_track_path(rel_path: str) -> Path: + """ + Resolve a relative path (e.g. 'Artist/Album/track.mp3') to an absolute path + inside MUSIC_DIR. Raises ValueError on path traversal. + """ + target = (MUSIC_DIR / rel_path).resolve() + if not str(target).startswith(str(MUSIC_DIR)): + raise ValueError("Path traversal detected.") + if not target.exists(): + raise FileNotFoundError(f"Not found: {rel_path}") + return target diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000..89f6e96 --- /dev/null +++ b/backend/main.py @@ -0,0 +1,133 @@ +""" +Music Orchestrator — FastAPI backend +""" + +from typing import Any + +from fastapi import FastAPI, HTTPException, Query +from fastapi.responses import FileResponse +from pydantic import BaseModel + +import search as music_search +import youtube +import library as lib + +# In production, nginx sits in front and serves the frontend statically, +# proxying /api/* to this process on 127.0.0.1:8000. CORS is not needed +# because all requests appear same-origin to the browser. +# In dev, Vite's proxy does the same job. No CORS middleware required. +app = FastAPI(title="Music Orchestrator", docs_url=None, redoc_url=None) + + +# ── Request / Response models ───────────────────────────────────────────────── + +class DownloadRequest(BaseModel): + videoId: str + trackName: str + artistName: str + albumName: str + trackNumber: int | None = None + year: str = "" + genre: str = "" + artworkUrl: str = "" + + +# ── Music search ────────────────────────────────────────────────────────────── + +@app.get("/api/search/music") +async def search_music(q: str = Query(..., min_length=1)) -> list[dict[str, Any]]: + """Search iTunes for song metadata.""" + try: + return await music_search.search_music(q) + except Exception as exc: + raise HTTPException(status_code=502, detail=f"iTunes search failed: {exc}") + + +# ── YouTube search ──────────────────────────────────────────────────────────── + +@app.get("/api/search/youtube") +async def search_youtube(q: str = Query(..., min_length=1)) -> list[dict[str, Any]]: + """Search YouTube and return top 5 results (no download).""" + try: + return await youtube.search_youtube(q) + except Exception as exc: + raise HTTPException(status_code=502, detail=f"YouTube search failed: {exc}") + + +# ── Download ────────────────────────────────────────────────────────────────── + +@app.post("/api/download") +async def start_download(req: DownloadRequest) -> dict[str, str]: + """ + Start an async download + tag job. Returns a job_id to poll. + Metadata comes from iTunes (req body), not from the YouTube video. + """ + job_id = await youtube.start_download( + video_id=req.videoId, + artist=req.artistName, + album=req.albumName, + track_name=req.trackName, + track_number=req.trackNumber, + year=req.year, + genre=req.genre, + artwork_url=req.artworkUrl, + ) + return {"jobId": job_id} + + +@app.get("/api/download/{job_id}/status") +def download_status(job_id: str) -> dict[str, Any]: + """Poll download job status. Returns { status, progress, filename, error }.""" + job = youtube.get_job_status(job_id) + if job is None: + raise HTTPException(status_code=404, detail="Job not found.") + return { + "status": job["status"], + "progress": job["progress"], + "filename": job.get("filename"), + "error": job.get("error"), + } + + +# ── Library ─────────────────────────────────────────────────────────────────── + +@app.get("/api/library") +def get_library() -> list[dict[str, Any]]: + """Return the full nested artist → album → tracks structure.""" + return lib.get_library() + + +@app.delete("/api/library") +def delete_library_item(path: str = Query(...)) -> dict[str, str]: + """ + Delete a file or folder inside the music library. + `path` is relative to MUSIC_DIR, e.g. 'Artist/Album/01 - Song.mp3' + """ + try: + lib.delete_path(path) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) + except FileNotFoundError as exc: + raise HTTPException(status_code=404, detail=str(exc)) + return {"deleted": path} + + +@app.get("/api/library/download") +def download_track(path: str = Query(...)) -> FileResponse: + """ + Stream a file from the music library to the browser as a download. + `path` is relative to MUSIC_DIR. + """ + try: + abs_path = lib.resolve_track_path(path) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) + except FileNotFoundError as exc: + raise HTTPException(status_code=404, detail=str(exc)) + + return FileResponse( + path=str(abs_path), + media_type="audio/mpeg", + filename=abs_path.name, + headers={"Content-Disposition": f'attachment; filename="{abs_path.name}"'}, + ) diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..13abeb9 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,8 @@ +fastapi==0.115.6 +uvicorn[standard]==0.34.0 +yt-dlp==2025.1.26 +mutagen==1.47.0 +httpx==0.28.1 +python-multipart==0.0.20 +python-dotenv==1.0.1 +aiofiles==24.1.0 diff --git a/backend/search.py b/backend/search.py new file mode 100644 index 0000000..9d89907 --- /dev/null +++ b/backend/search.py @@ -0,0 +1,45 @@ +""" +Music metadata search via the iTunes Search API. +Free, no auth, returns track info + album art URLs. +""" + +import httpx +from typing import Any + +ITUNES_URL = "https://itunes.apple.com/search" + + +async def search_music(query: str, limit: int = 10) -> list[dict[str, Any]]: + params = { + "term": query, + "media": "music", + "entity": "song", + "limit": limit, + } + + async with httpx.AsyncClient(timeout=10) as client: + response = await client.get(ITUNES_URL, params=params) + response.raise_for_status() + data = response.json() + + results = [] + for item in data.get("results", []): + # Upgrade artwork URL to 600x600 (iTunes returns 100x100 by default) + art_url = item.get("artworkUrl100", "") + if art_url: + art_url = art_url.replace("100x100bb", "600x600bb") + + results.append({ + "trackId": item.get("trackId"), + "trackName": item.get("trackName", ""), + "artistName": item.get("artistName", ""), + "collectionName": item.get("collectionName", ""), + "trackNumber": item.get("trackNumber"), + "discNumber": item.get("discNumber"), + "releaseDate": item.get("releaseDate", "")[:4], # just the year + "genre": item.get("primaryGenreName", ""), + "artworkUrl": art_url, + "previewUrl": item.get("previewUrl", ""), + }) + + return results diff --git a/backend/tagger.py b/backend/tagger.py new file mode 100644 index 0000000..a3f6ced --- /dev/null +++ b/backend/tagger.py @@ -0,0 +1,65 @@ +""" +Embeds ID3 metadata into a downloaded MP3 using mutagen. +Fetches album art from the iTunes artwork URL and embeds it as APIC. +""" + +import httpx +from pathlib import Path +from mutagen.id3 import ( + ID3, ID3NoHeaderError, + TIT2, TPE1, TALB, TRCK, TDRC, TCON, APIC, +) + + +async def tag_file( + mp3_path: Path, + track_name: str, + artist_name: str, + album_name: str, + track_number: int | None, + year: str, + genre: str, + artwork_url: str, +) -> None: + """Write ID3v2 tags to mp3_path. Fetches artwork from artwork_url.""" + + # Load or create ID3 header + try: + tags = ID3(str(mp3_path)) + except ID3NoHeaderError: + tags = ID3() + + tags.delall("TIT2"); tags["TIT2"] = TIT2(encoding=3, text=track_name) + tags.delall("TPE1"); tags["TPE1"] = TPE1(encoding=3, text=artist_name) + tags.delall("TALB"); tags["TALB"] = TALB(encoding=3, text=album_name) + tags.delall("TDRC"); tags["TDRC"] = TDRC(encoding=3, text=year) + tags.delall("TCON"); tags["TCON"] = TCON(encoding=3, text=genre) + + if track_number is not None: + tags.delall("TRCK") + tags["TRCK"] = TRCK(encoding=3, text=str(track_number)) + + # Fetch and embed album art + if artwork_url: + art_data = await _fetch_artwork(artwork_url) + if art_data: + tags.delall("APIC") + tags["APIC"] = APIC( + encoding=3, + mime="image/jpeg", + type=3, # front cover + desc="Cover", + data=art_data, + ) + + tags.save(str(mp3_path), v2_version=3) # ID3v2.3 for broadest device compatibility + + +async def _fetch_artwork(url: str) -> bytes | None: + try: + async with httpx.AsyncClient(timeout=15) as client: + r = await client.get(url) + r.raise_for_status() + return r.content + except Exception: + return None diff --git a/backend/youtube.py b/backend/youtube.py new file mode 100644 index 0000000..7e55167 --- /dev/null +++ b/backend/youtube.py @@ -0,0 +1,207 @@ +""" +YouTube search and audio download via yt-dlp. +Search uses yt-dlp's built-in ytsearch (no API key needed). +Downloads extract audio as MP3 using ffmpeg post-processing. +""" + +import asyncio +import re +import uuid +from pathlib import Path +from typing import Any + +import yt_dlp + +from config import MUSIC_DIR, TEMP_DIR + +# In-memory store of active download jobs { job_id: { status, progress, ... } } +_jobs: dict[str, dict[str, Any]] = {} + + +# ── Search ──────────────────────────────────────────────────────────────────── + +async def search_youtube(query: str, max_results: int = 5) -> list[dict[str, Any]]: + """Return top N YouTube results for a query without downloading anything.""" + ydl_opts = { + "quiet": True, + "no_warnings": True, + "extract_flat": True, + "skip_download": True, + } + search_query = f"ytsearch{max_results}:{query}" + loop = asyncio.get_event_loop() + return await loop.run_in_executor(None, _yt_search, search_query, ydl_opts) + + +def _yt_search(query: str, opts: dict) -> list[dict[str, Any]]: + with yt_dlp.YoutubeDL(opts) as ydl: + info = ydl.extract_info(query, download=False) + + entries = info.get("entries", []) if info else [] + results = [] + for entry in entries: + if not entry: + continue + video_id = entry.get("id", "") + results.append({ + "videoId": video_id, + "title": entry.get("title", ""), + "channel": entry.get("uploader") or entry.get("channel", ""), + "duration": _format_duration(entry.get("duration")), + "thumbnailUrl": f"https://i.ytimg.com/vi/{video_id}/mqdefault.jpg", + "watchUrl": f"https://www.youtube.com/watch?v={video_id}", + }) + return results + + +def _format_duration(seconds: int | None) -> str: + if not seconds: + return "" + m, s = divmod(int(seconds), 60) + h, m = divmod(m, 60) + if h: + return f"{h}:{m:02d}:{s:02d}" + return f"{m}:{s:02d}" + + +# ── Download ────────────────────────────────────────────────────────────────── + +async def start_download( + video_id: str, + artist: str, + album: str, + track_name: str, + track_number: int | None, + year: str, + genre: str, + artwork_url: str, +) -> str: + """ + Kick off a background download + tag job. Returns a job_id to poll. + Tagging runs inside the same background task after the download completes. + """ + job_id = str(uuid.uuid4()) + _jobs[job_id] = { + "status": "queued", + "progress": 0, + "filename": None, + "error": None, + } + + asyncio.create_task( + _run_download( + job_id, video_id, artist, album, track_name, + track_number, year, genre, artwork_url, + ) + ) + return job_id + + +async def _run_download( + job_id: str, + video_id: str, + artist: str, + album: str, + track_name: str, + track_number: int | None, + year: str, + genre: str, + artwork_url: str, +) -> None: + import tagger # local import to avoid circular deps + + _jobs[job_id]["status"] = "downloading" + + safe_artist = _safe_name(artist) + safe_album = _safe_name(album) + safe_title = _safe_name(track_name) + prefix = f"{track_number:02d} - " if track_number else "" + filename_stem = f"{prefix}{safe_title}" + + dest_dir = MUSIC_DIR / safe_artist / safe_album + dest_dir.mkdir(parents=True, exist_ok=True) + + temp_out = str(TEMP_DIR / f"{job_id}.%(ext)s") + final_path = dest_dir / f"{filename_stem}.mp3" + + ydl_opts = { + "quiet": True, + "no_warnings": True, + "format": "bestaudio/best", + "outtmpl": temp_out, + "postprocessors": [{ + "key": "FFmpegExtractAudio", + "preferredcodec": "mp3", + "preferredquality": "0", + }], + "progress_hooks": [_make_progress_hook(job_id)], + } + + url = f"https://www.youtube.com/watch?v={video_id}" + loop = asyncio.get_event_loop() + + try: + await loop.run_in_executor(None, _yt_download, url, ydl_opts) + except Exception as exc: + _jobs[job_id]["status"] = "error" + _jobs[job_id]["error"] = str(exc) + return + + # Move temp file to library + temp_mp3 = TEMP_DIR / f"{job_id}.mp3" + if not temp_mp3.exists(): + _jobs[job_id]["status"] = "error" + _jobs[job_id]["error"] = "Converted file not found — is ffmpeg installed?" + return + + temp_mp3.rename(final_path) + + # Write ID3 tags + embed album art + _jobs[job_id]["status"] = "tagging" + try: + await tagger.tag_file( + mp3_path=final_path, + track_name=track_name, + artist_name=artist, + album_name=album, + track_number=track_number, + year=year, + genre=genre, + artwork_url=artwork_url, + ) + except Exception as exc: + # Tagging failure is non-fatal — file is still saved + _jobs[job_id]["error"] = f"Tagging warning: {exc}" + + _jobs[job_id]["status"] = "done" + _jobs[job_id]["progress"] = 100 + _jobs[job_id]["filename"] = str(final_path.relative_to(MUSIC_DIR)) + + +def _yt_download(url: str, opts: dict) -> None: + with yt_dlp.YoutubeDL(opts) as ydl: + ydl.download([url]) + + +def _make_progress_hook(job_id: str): + def hook(d: dict) -> None: + if d["status"] == "downloading": + total = d.get("total_bytes") or d.get("total_bytes_estimate", 0) + downloaded = d.get("downloaded_bytes", 0) + if total: + _jobs[job_id]["progress"] = int(downloaded / total * 85) + elif d["status"] == "finished": + _jobs[job_id]["progress"] = 85 # tagging will push to 100 + return hook + + +def get_job_status(job_id: str) -> dict[str, Any] | None: + return _jobs.get(job_id) + + +def _safe_name(name: str) -> str: + """Remove characters that are illegal in filenames/directory names.""" + name = name.strip() + name = re.sub(r'[<>:"/\\|?*]', "", name) + name = re.sub(r"\s+", " ", name).strip(". ") + return name or "Unknown" diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 0000000..8fee9b0 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,173 @@ +#!/usr/bin/env bash +# Music Orchestrator — production deploy script for Ubuntu +# Run as a regular user with sudo access. Do NOT run as root. +# +# First run: bash deploy.sh +# Update: bash deploy.sh (re-run at any time — it's idempotent) + +set -euo pipefail + +# ── Config ───────────────────────────────────────────────────────────────────── +INSTALL_DIR="/opt/music-orchestrator" +SERVICE_NAME="music-orchestrator" +SERVICE_USER="$(whoami)" +NGINX_SITE="music-orchestrator" + +# Where your Plex library lives on this machine. +# Can be changed later by editing $INSTALL_DIR/backend/.env +DEFAULT_MUSIC_DIR="$HOME/Music" + +# Colours +GREEN="\033[0;32m"; YELLOW="\033[0;33m"; RED="\033[0;31m"; NC="\033[0m" +info() { echo -e "${GREEN}[+]${NC} $*"; } +warn() { echo -e "${YELLOW}[!]${NC} $*"; } +section() { echo -e "\n${GREEN}── $* ──────────────────────────────────────────────────${NC}"; } + +# ── Checks ───────────────────────────────────────────────────────────────────── +section "Checking prerequisites" + +if [[ $EUID -eq 0 ]]; then + echo -e "${RED}[✗]${NC} Do not run this script as root. Run as your regular user." + exit 1 +fi + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +if [[ ! -f "$SCRIPT_DIR/backend/main.py" ]]; then + echo -e "${RED}[✗]${NC} Run this script from the music-orchestrator project root." + exit 1 +fi + +# ── System dependencies ──────────────────────────────────────────────────────── +section "Installing system packages" + +sudo apt-get update -qq +sudo apt-get install -y \ + python3 python3-pip python3-venv \ + ffmpeg \ + nginx \ + nodejs npm \ + --no-install-recommends + +info "System packages ready" + +# ── Install directory ────────────────────────────────────────────────────────── +section "Setting up install directory ($INSTALL_DIR)" + +sudo mkdir -p "$INSTALL_DIR" +sudo chown "$SERVICE_USER:$SERVICE_USER" "$INSTALL_DIR" + +# Sync project files (rsync excludes dev artifacts) +rsync -a --delete \ + --exclude='.git' \ + --exclude='node_modules' \ + --exclude='frontend/dist' \ + --exclude='backend/venv' \ + --exclude='backend/__pycache__' \ + --exclude='backend/tmp' \ + --exclude='backend/Music' \ + --exclude='.env' \ + "$SCRIPT_DIR/" "$INSTALL_DIR/" + +info "Project files synced to $INSTALL_DIR" + +# ── Backend: Python virtualenv ───────────────────────────────────────────────── +section "Setting up Python virtual environment" + +python3 -m venv "$INSTALL_DIR/backend/venv" +"$INSTALL_DIR/backend/venv/bin/pip" install --quiet --upgrade pip +"$INSTALL_DIR/backend/venv/bin/pip" install --quiet -r "$INSTALL_DIR/backend/requirements.txt" + +info "Python dependencies installed" + +# ── Backend: .env ────────────────────────────────────────────────────────────── +section "Configuring backend .env" + +ENV_FILE="$INSTALL_DIR/backend/.env" + +if [[ ! -f "$ENV_FILE" ]]; then + # First install — prompt for music dir + echo "" + warn "Where is your Plex music library? (press Enter for default: $DEFAULT_MUSIC_DIR)" + read -r -p " Music directory: " MUSIC_DIR_INPUT + MUSIC_DIR="${MUSIC_DIR_INPUT:-$DEFAULT_MUSIC_DIR}" + + mkdir -p "$MUSIC_DIR" + + cat > "$ENV_FILE" </dev/null | cut -d= -f2 || echo "/tmp/music-orchestrator") +mkdir -p "$TEMP_DIR" + +# ── Frontend: build ──────────────────────────────────────────────────────────── +section "Building React frontend" + +cd "$INSTALL_DIR/frontend" +npm install --silent +npm run build + +info "Frontend built to $INSTALL_DIR/frontend/dist" + +# ── nginx ────────────────────────────────────────────────────────────────────── +section "Configuring nginx" + +sudo cp "$INSTALL_DIR/nginx.conf" "/etc/nginx/sites-available/$NGINX_SITE" + +# Enable the site, remove default if present +sudo ln -sf "/etc/nginx/sites-available/$NGINX_SITE" "/etc/nginx/sites-enabled/$NGINX_SITE" +sudo rm -f /etc/nginx/sites-enabled/default + +sudo nginx -t # validate config before reloading +sudo systemctl enable nginx --quiet +sudo systemctl reload nginx + +info "nginx configured and reloaded" + +# ── systemd service ──────────────────────────────────────────────────────────── +section "Installing systemd service" + +# The service uses %i (instance name) for User= — we pass the username as the instance +SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}@.service" +sudo cp "$INSTALL_DIR/music-orchestrator.service" "$SERVICE_FILE" + +sudo systemctl daemon-reload +sudo systemctl enable "${SERVICE_NAME}@${SERVICE_USER}" --quiet +sudo systemctl restart "${SERVICE_NAME}@${SERVICE_USER}" + +# Wait a moment and check it started cleanly +sleep 2 +if systemctl is-active --quiet "${SERVICE_NAME}@${SERVICE_USER}"; then + info "Service started successfully" +else + echo -e "${RED}[✗]${NC} Service failed to start. Check logs with:" + echo " journalctl -u ${SERVICE_NAME}@${SERVICE_USER} -n 50" + exit 1 +fi + +# ── Done ─────────────────────────────────────────────────────────────────────── +section "Deploy complete" + +LOCAL_IP=$(hostname -I | awk '{print $1}') + +echo "" +echo -e " ${GREEN}Music Orchestrator is running at:${NC}" +echo -e " http://$LOCAL_IP" +echo "" +echo " Useful commands:" +echo " View logs: journalctl -u ${SERVICE_NAME}@${SERVICE_USER} -f" +echo " Restart API: sudo systemctl restart ${SERVICE_NAME}@${SERVICE_USER}" +echo " Edit config: nano $ENV_FILE" +echo "" +echo " To update after code changes: just re-run bash deploy.sh" +echo "" diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..d2e7761 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..072a57e --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + frontend + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..fcda889 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,3308 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "lucide-react": "^0.575.0", + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.48.0", + "vite": "^7.3.1" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@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.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.3", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.3.tgz", + "integrity": "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.10.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.13.tgz", + "integrity": "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz", + "integrity": "sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/type-utils": "8.56.0", + "@typescript-eslint/utils": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.56.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.0.tgz", + "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.0.tgz", + "integrity": "sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.56.0", + "@typescript-eslint/types": "^8.56.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.0.tgz", + "integrity": "sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.0.tgz", + "integrity": "sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.0.tgz", + "integrity": "sha512-qX2L3HWOU2nuDs6GzglBeuFXviDODreS58tLY/BALPC7iu3Fa+J7EOTwnX9PdNBxUI7Uh0ntP0YWGnxCkXzmfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/utils": "8.56.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.0.tgz", + "integrity": "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.0.tgz", + "integrity": "sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.56.0", + "@typescript-eslint/tsconfig-utils": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", + "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.6.tgz", + "integrity": "sha512-kQAVowdR33euIqeA0+VZTDqU+qo1IeVY+hrKYtZMio3Pg0P0vuh/kwRylLUddJhB6pf3q/botcOvRtx4IN1wqQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.0.tgz", + "integrity": "sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.0.tgz", + "integrity": "sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.4.tgz", + "integrity": "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001772", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001772.tgz", + "integrity": "sha512-mIwLZICj+ntVTw4BT2zfp+yu/AqV6GMKfJVJMx3MwPxs+uk/uj2GLl2dH8LQbjiLDX66amCga5nKFyDgRR43kg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.302", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", + "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.3", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.3.tgz", + "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.3", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.575.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.575.0.tgz", + "integrity": "sha512-VuXgKZrk0uiDlWjGGXmKV6MSk9Yy4l10qgVvzGn2AWBx1Ylt0iBexKOAoA6I7JO3m+M9oeovJd3yYENfkUbOeg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/minimatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", + "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.0.tgz", + "integrity": "sha512-c7toRLrotJ9oixgdW7liukZpsnq5CZ7PuKztubGYlNppuTqhIoWfhgHo/7EU0v06gS2l/x0i2NEFK1qMIf0rIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.56.0", + "@typescript-eslint/parser": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/utils": "8.56.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..9bc5a9e --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,31 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "lucide-react": "^0.575.0", + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.48.0", + "vite": "^7.3.1" + } +} diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/App.css b/frontend/src/App.css new file mode 100644 index 0000000..71c7847 --- /dev/null +++ b/frontend/src/App.css @@ -0,0 +1 @@ +/* App-level styles — main styles live in index.css */ diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..c068006 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,79 @@ +import { useState } from 'react'; +import { Music, Library, Download } from 'lucide-react'; +import MusicSearch from './components/MusicSearch'; +import YouTubePanel from './components/YouTubePanel'; +import LibraryView from './components/Library'; +import type { MusicResult } from './api/types'; +import './App.css'; + +type Tab = 'search' | 'library'; + +export default function App() { + const [tab, setTab] = useState('search'); + const [selectedTrack, setSelectedTrack] = useState(null); + const [libraryRefreshKey, setLibraryRefreshKey] = useState(0); + + function handleTrackSelect(track: MusicResult) { + setSelectedTrack(track); + } + + function handleDownloadDone() { + setTab('library'); + setLibraryRefreshKey(k => k + 1); + setSelectedTrack(null); + } + + return ( +
+ + +
+ {tab === 'search' && ( +
+
+

Find a Song

+ +
+ + {selectedTrack && ( +
+

Choose a Video

+ +
+ )} +
+ )} + + {tab === 'library' && ( +
+

Your Library

+ +
+ )} +
+
+ ); +} diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts new file mode 100644 index 0000000..e785cfa --- /dev/null +++ b/frontend/src/api/client.ts @@ -0,0 +1,51 @@ +import type { + MusicResult, YouTubeResult, DownloadRequest, + DownloadStatus, Artist, +} from './types'; + +const BASE = '/api'; + +export async function searchMusic(query: string): Promise { + const res = await fetch(`${BASE}/search/music?q=${encodeURIComponent(query)}`); + if (!res.ok) throw new Error(await res.text()); + return res.json(); +} + +export async function searchYouTube(query: string): Promise { + const res = await fetch(`${BASE}/search/youtube?q=${encodeURIComponent(query)}`); + if (!res.ok) throw new Error(await res.text()); + return res.json(); +} + +export async function startDownload(req: DownloadRequest): Promise<{ jobId: string }> { + const res = await fetch(`${BASE}/download`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(req), + }); + if (!res.ok) throw new Error(await res.text()); + return res.json(); +} + +export async function getDownloadStatus(jobId: string): Promise { + const res = await fetch(`${BASE}/download/${jobId}/status`); + if (!res.ok) throw new Error(await res.text()); + return res.json(); +} + +export async function getLibrary(): Promise { + const res = await fetch(`${BASE}/library`); + if (!res.ok) throw new Error(await res.text()); + return res.json(); +} + +export async function deleteLibraryItem(path: string): Promise { + const res = await fetch(`${BASE}/library?path=${encodeURIComponent(path)}`, { + method: 'DELETE', + }); + if (!res.ok) throw new Error(await res.text()); +} + +export function getDownloadUrl(path: string): string { + return `${BASE}/library/download?path=${encodeURIComponent(path)}`; +} diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts new file mode 100644 index 0000000..bd3e6c8 --- /dev/null +++ b/frontend/src/api/types.ts @@ -0,0 +1,54 @@ +export interface MusicResult { + trackId: number; + trackName: string; + artistName: string; + collectionName: string; + trackNumber: number | null; + releaseDate: string; + genre: string; + artworkUrl: string; + previewUrl: string; +} + +export interface YouTubeResult { + videoId: string; + title: string; + channel: string; + duration: string; + thumbnailUrl: string; + watchUrl: string; +} + +export interface DownloadRequest { + videoId: string; + trackName: string; + artistName: string; + albumName: string; + trackNumber: number | null; + year: string; + genre: string; + artworkUrl: string; +} + +export interface DownloadStatus { + status: 'queued' | 'downloading' | 'tagging' | 'done' | 'error'; + progress: number; + filename: string | null; + error: string | null; +} + +export interface Track { + filename: string; + path: string; + size: number; +} + +export interface Album { + album: string; + tracks: Track[]; +} + +export interface Artist { + artist: string; + albums: Album[]; +} diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/frontend/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/components/Library.tsx b/frontend/src/components/Library.tsx new file mode 100644 index 0000000..6fa6d3a --- /dev/null +++ b/frontend/src/components/Library.tsx @@ -0,0 +1,199 @@ +import { useEffect, useState } from 'react'; +import { + RefreshCw, Trash2, Download, ChevronRight, + Music, Disc, FolderOpen, Loader2, +} from 'lucide-react'; +import { getLibrary, deleteLibraryItem, getDownloadUrl } from '../api/client'; +import type { Artist, Album, Track } from '../api/types'; + +function formatSize(bytes: number): string { + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`; + return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; +} + +export default function Library() { + const [artists, setArtists] = useState([]); + const [loading, setLoading] = useState(true); + const [selectedArtist, setSelectedArtist] = useState(null); + const [selectedAlbum, setSelectedAlbum] = useState(null); + const [deleting, setDeleting] = useState(null); + + async function load() { + setLoading(true); + try { + const data = await getLibrary(); + setArtists(data); + // Re-sync selections in case they were deleted + setSelectedArtist(a => data.find(x => x.artist === a?.artist) ?? null); + setSelectedAlbum(al => { + const parent = data.find(x => x.artist === selectedArtist?.artist); + return parent?.albums.find(x => x.album === al?.album) ?? null; + }); + } finally { + setLoading(false); + } + } + + useEffect(() => { load(); }, []); + + async function handleDelete(path: string, label: string) { + if (!confirm(`Delete "${label}" from your library?`)) return; + setDeleting(path); + try { + await deleteLibraryItem(path); + await load(); + } catch { + alert('Delete failed.'); + } finally { + setDeleting(null); + } + } + + if (loading) { + return ( +
+ + Loading library… +
+ ); + } + + if (artists.length === 0) { + return ( +
+ +

Your library is empty.

+

Search for songs and download them — they'll appear here.

+
+ ); + } + + return ( +
+ {/* Artist column */} +
+
+ Artists + +
+
    + {artists.map(artist => ( +
  • { + setSelectedArtist(artist); + setSelectedAlbum(null); + }} + > + + {artist.artist} +
    e.stopPropagation()}> + +
    + +
  • + ))} +
+
+ + {/* Album column */} +
+
+ Albums +
+ {selectedArtist ? ( +
    + {selectedArtist.albums.map(album => ( +
  • setSelectedAlbum(album)} + > + + {album.album} +
    e.stopPropagation()}> + +
    + +
  • + ))} +
+ ) : ( +
+ + Select an artist +
+ )} +
+ + {/* Tracks column */} +
+
+ Tracks +
+ {selectedAlbum ? ( +
    + {selectedAlbum.tracks.map((track: Track) => ( +
  • + +
    + {track.filename} + {formatSize(track.size)} +
    +
    e.stopPropagation()}> + + + + +
    +
  • + ))} +
+ ) : ( +
+ + Select an album +
+ )} +
+
+ ); +} diff --git a/frontend/src/components/MusicSearch.tsx b/frontend/src/components/MusicSearch.tsx new file mode 100644 index 0000000..5febad6 --- /dev/null +++ b/frontend/src/components/MusicSearch.tsx @@ -0,0 +1,92 @@ +import { useState, useRef } from 'react'; +import { Search, Loader2, Music } from 'lucide-react'; +import { searchMusic } from '../api/client'; +import type { MusicResult } from '../api/types'; + +interface Props { + onSelect: (track: MusicResult) => void; +} + +export default function MusicSearch({ onSelect }: Props) { + const [query, setQuery] = useState(''); + const [results, setResults] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + const inputRef = useRef(null); + + async function handleSearch(e: React.FormEvent) { + e.preventDefault(); + if (!query.trim()) return; + setLoading(true); + setError(''); + try { + const data = await searchMusic(query.trim()); + setResults(data); + if (data.length === 0) setError('No results found. Try a different search.'); + } catch { + setError('Search failed. Make sure the backend is running.'); + } finally { + setLoading(false); + } + } + + return ( +
+
+
+ + setQuery(e.target.value)} + autoFocus + /> +
+ +
+ + {error &&

{error}

} + + {results.length > 0 && ( +
    + {results.map(track => ( +
  • onSelect(track)} + > + {track.artworkUrl ? ( + {track.collectionName} + ) : ( +
    + +
    + )} +
    + {track.trackName} + {track.artistName} + + {track.collectionName} + {track.releaseDate ? ` · ${track.releaseDate}` : ''} + {track.genre ? ` · ${track.genre}` : ''} + +
    + {track.trackNumber && ( + #{track.trackNumber} + )} +
  • + ))} +
+ )} +
+ ); +} diff --git a/frontend/src/components/YouTubePanel.tsx b/frontend/src/components/YouTubePanel.tsx new file mode 100644 index 0000000..541865d --- /dev/null +++ b/frontend/src/components/YouTubePanel.tsx @@ -0,0 +1,187 @@ +import { useEffect, useState } from 'react'; +import { Loader2, Play, CheckCircle2, Youtube } from 'lucide-react'; +import { searchYouTube, startDownload, getDownloadStatus } from '../api/client'; +import type { MusicResult, YouTubeResult, DownloadStatus } from '../api/types'; + +interface Props { + track: MusicResult; + onDownloadDone: () => void; +} + +export default function YouTubePanel({ track, onDownloadDone }: Props) { + const [results, setResults] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + const [selectedVideo, setSelectedVideo] = useState(null); + const [jobId, setJobId] = useState(null); + const [jobStatus, setJobStatus] = useState(null); + const [downloading, setDownloading] = useState(false); + + // Search YouTube when track changes + useEffect(() => { + setResults([]); + setSelectedVideo(null); + setJobId(null); + setJobStatus(null); + setLoading(true); + setError(''); + + const query = `${track.artistName} ${track.trackName} official audio`; + searchYouTube(query) + .then(data => setResults(data)) + .catch(() => setError('YouTube search failed.')) + .finally(() => setLoading(false)); + }, [track.trackId]); + + // Poll job status + useEffect(() => { + if (!jobId) return; + const interval = setInterval(async () => { + try { + const status = await getDownloadStatus(jobId); + setJobStatus(status); + if (status.status === 'done' || status.status === 'error') { + clearInterval(interval); + setDownloading(false); + if (status.status === 'done') { + setTimeout(onDownloadDone, 1500); + } + } + } catch { + clearInterval(interval); + } + }, 2000); + return () => clearInterval(interval); + }, [jobId]); + + async function handleDownload() { + if (!selectedVideo) return; + setDownloading(true); + setJobStatus(null); + try { + const { jobId: id } = await startDownload({ + videoId: selectedVideo.videoId, + trackName: track.trackName, + artistName: track.artistName, + albumName: track.collectionName, + trackNumber: track.trackNumber, + year: track.releaseDate, + genre: track.genre, + artworkUrl: track.artworkUrl, + }); + setJobId(id); + } catch { + setDownloading(false); + setError('Failed to start download.'); + } + } + + if (loading) { + return ( +
+ + Searching YouTube… +
+ ); + } + + if (error) return

{error}

; + + const isDone = jobStatus?.status === 'done'; + const hasError = jobStatus?.status === 'error'; + + return ( +
+ {/* Selected track header */} +
+ {track.artworkUrl && ( + {track.collectionName} + )} +
+

{track.trackName}

+

{track.artistName} · {track.collectionName}

+
+
+ + {/* YouTube results */} +

Select a YouTube video to download

+
    + {results.map(video => ( +
  • setSelectedVideo(video)} + > +
    + {video.title} + +
    +
    + {video.title} + {video.channel} · {video.duration} +
    + {selectedVideo?.videoId === video.videoId && ( + + )} +
  • + ))} +
+ + {/* Embedded player */} + {selectedVideo && ( +
+