From aea00bd36ffcc2bf01fba10ad0a63461a0487dff Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Mon, 25 Mar 2024 17:33:28 +0100 Subject: [PATCH 1/7] feat(logs): `logs` command to watch deployment logs --- bun.lockb | Bin 227706 -> 228535 bytes package.json | 4 +- src/commands/logs.mjs | 91 ++++++++++++++++++++++++++ src/index.mjs | 2 + src/utils/logs.mjs | 144 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 240 insertions(+), 1 deletion(-) create mode 100644 src/commands/logs.mjs create mode 100644 src/utils/logs.mjs diff --git a/bun.lockb b/bun.lockb index 932f3b490b425292157db1b5eee5fd13378441a1..21a1561d61c36d590782bdc649373f2c406f9e54 100755 GIT binary patch delta 51999 zcmeFa37pPV|Nnno7sFf_OV%-jBE(>9Gh>*cC}Zr(zBCwS%2;NsF|Mg>V+nn5OeN7q z67DuC?b}o;byp-6)qPVb?Nsw2H}>%7nQKJRnR=kuBI zneV=cdf;f(k|qsX&P@ICtctTLFaFn!vzKrG@W`bR>4&D@-hbxBHI`k|yUU1%^=B0G z>C}ahEOG>`l|eq?$(#|k{!YbxzKS6-va`l!Wm5yf&1pl1 zD%eDUipZ4_wxS9pY(+znrQm;nuZ%p5F8dGAt06~^o9NclH`3a-MXDoRJiRG03Vi~- z52ruR_-vmq9+%1B7_F(nFZfc0Vb5TfH8yrcR%34}>UeSn zwW-6OA=U7Hq&ldXRQWg2CC7PupR=sp1831Ajr_1PAy7Zmma@|3BjtxzJXsGZKaHes zs-`be-3!4>kMraMRIBne&?PH+GKf@t;d*{r|H`U9AOAup8<#Gh z_eym;uE&rJCI4I`1@kw1`YNO*Y!;HD`Onm_ei@lDe9~yjXUidc>eRFyx}SX26Lq%i zl6?6&6l4(jBan*yu1GZyZm5lyKWWrBx1w>QGA545^!dJVb{4A|hz&R&6-$nuSI5?# zJ!Z@_StXroM>EsuP`s*tx-+17O7OwD)`eS~HN~rjCU_;Xa$C0?o;A@ovc4S$^F1YZ zjL&z}(|b~z{E{&>V?;XX6Ai3gc%H|N&6t>#o#D%7J&lMRHTFyLHF4w9CuYVHrT_Ny zcaW-QCz3cRbu#jwaXLg)4L#V%j($;N>+W#zA#{_e%+H9m1KfaAHN$h#M`X~4bDCH_ z6RG%k5UG5IF?!6n;l374ZT?`dTzBe|mpjMVb*ye9U zGFAE4B9%W8$&BO=Kx(12M{3^dBAMF!QeOTKG^`GNj#R*AjmyaxnU(8%sI?u?CZzmy zi!0Cdmzo%4N)?ZPKW)pBcl+tos( zYHQ!Y$tjr}3eVNg`1lWZ@yKx#GsaH#`9_Y-ZBAr5O(QEbEb44K{-`JKMXHNTp*x^4 zUF-mdI$4qRL&dw=N&PO(=6-_Iq-Cb(j+-*p=S%KpqwxxOMU@*;)Kt2=b;!7}BbXtd zZ%R)3`0*^f0KDw{J*=ZLkaA-`r0UHYo0*Z5H8D49?5HtWqccMjzoh|nuMgdlJkrxn zXYRNWS7?PT?q$0>8>xz__O|l!+~IWZ)UNUo<d(j> zpOclH?z`v`bu{G5uXCxbFe7);#H=y7zN((y&NVo-orSlp(HRqCC*@@MM&-t4Or4mM z-mIT(=tMIfHFO3FNE6Cge*t9AfvWn~*x8t0L9#=yBsm%5(G4 zD{E)T%1zJ7NuTD+r9u_#;;boEH8dJc3!yXBsTnR(+~aQ4xSZIG^bwh@(=EU4GM}#s z`J>2J_dnrEgQF@xk498mHd4bIjI4m{-E-+Yo5-)^8GMI9 zbqwO^kSYp!9oUc5_;w)G@FPfdgbgq!V|2#Ujp(vBNOkmBmeu=TZg+_J@bc#*q#~@B zYe!^sAfcYuN2zvw$RDS1dJA#+dHKLo4F~~VcbufmGS44h9;A%r1 z#|20_2gjyyw)~F{7)3_-B>Bn7k-wG#>hZYo8C1m;3?ZeSADv)F_8C(8+8jH=rAT=o z9GZ2omnW)vdrdGr7 z6-@Zb48yGfg6sDRaBrAHhqEZ z$N(q3T-8u2)ybisF0^tVQl(}rvR3WMSGNy%-Ob6En1g?Pd(dTf@LIwbc`H(S^b$M4 zkw|rO>S7x)#g^J-@CCY-!)A1bAIjekp>8EyXA8XQ>5n4i_?M_y4&H*4KVI=VlED@8 zSeC}<<<@^gJXvLht#3@mAFIggjG+HlkLRx3_v4XgbPwt3FMob*vXAFFNR z#Eg-?uN{Ads&)1+vxB_(M%&gK=<36QRaVb*5-ZdXt-i_nbT(3(@}pQ)MXo`fjhus& ziyph#UKGwi*O12`)d%-J6xjz|LvMvtxv&ev_RNRt@^!b^sc3bpySEO?uS-Ic8HH5C zBXV;ya!2@l#nI)W;p4`Q8J?b-v6zBtVD0Vp7#@YxW;_#F7g=Q-UwSwYPO$ncy$( zJYPRK*uS{Xhd1*B&d!8{;wTBuj`~UdcbtL-DZw%kwo0F~vtE3lON3LZVY2^rC$(Wp zpk@hYK1!xj)G#G@YYCsPE0y@23Jv4^-#e-2rv%%S^!a+b9G&wobc)VTi5QL#+B-Yq zlOooV>gcBa73Kox1)b*`B?t3}2=&tM>}(Vt+>WO4M>xIe$47jN*49~6KPk|nl=FOJ zjF}5{-@0cvng;DUBv7-oGe0&tcpG7P0c51>!GEL4#PxBYQ5mO8ljPti){IJ5n|J7@Az|ca}Db z_kZW4woC~ovW2Q6G&Uz85=Db?`{=*T3ARcJz6sYHuB7HC*r}4O0$(poh(u}YX6@{q zP+W>Tb5$xkU0Npx$5pm{3Oc=rnT<|S>y+T<9!G0TebXw|qPYC(&vpvpQ~c|kqWF~H zesa`P8l9gI$P@K)AuRHQQQu2r!6#j>tqQn&c zt4^>@ihr+@+9oAfj{Pu+d?LDOyuXW61h)XL2b`Uvx6nGc7Mkan8a5*E9eo|{6eZyr zxGq?@Q4(lW)9KPSIXHw3r?Fc}uh#Lw0yOo#gceTlX*2~{gc}2ApY8Ltb1j*ih(Tzb zoTxTQ!JA2?Se}O8MYElv;b42ul1+|hL;WrJ{Mv-seAwD#0iOabA4%?VzZ z5~xtu>2hIm@PfMGI46E0W}(G8I}(%p&pE-4DG}eHH*;tDe9q!#R6$G;`vcL~;o2t! zuOp>kc3l;G2F-SRXZ!fz5wwnOp4LU%^GrWlY`43)uTwKLP0|DhFG}6xr|h@ z+lK172~EwrdqMD@Xj-hc{W9lU-w{o8v@6=FyjfnJ+qb||=esQ)ht$qt!>@fKJD5m! zSDS&RX4(IkmiN(41x#h)*=q9E#c1-6TXn?sXm+o8o0K}hkO|aEoQMi__DuGgaZW+k zl;FK^ntH|ve+#NktiBC`yP{j2u&j(Y?{Rf8nt%j z_f8JJ%;2@9Gva#j5w+sI12;HKDQiqLtVZjKX5;T;G));k)0$&x zS_fB?<)>);Q!VWiBDt)U!tABQ2WNX43zu$eM{`g6pdbHh1a9|&ebKtOnyR}QP2(Z{ zmLx=?*t)eKtFQ$zu`0$@Ku0uoi-t+TiKH~D67Kxo>si^5ZArW+&pK-!n*4&ZXlfUl z_RvyJg;w#ws%*0wKl>P=HUv%GuxsiDG+R!VAA6cz(@B`w!4P@=kW(}uCHN>Dm3wP2 zNF%avS7bytwAN0y)=9x@NLgoTA|FB{;?$d_@e!rzbZ2+1_aUWz;Si3)`_VLKc6kPv z2KAD|u~kAOiVW@5$!aw1c<#9wd>Kt$zzqor{tKK^Lz0837g!6Naelmino}?&C3rg= z!HkQwaCV_-8F9tXFg{S@LZ{2n1 znrdNzwvG>!@8OinQl%*#*oY#YJ2nCwO^E@CG=oJG-0gLDRs4uK(+>%~>~SRS!Y4UDqmDY%l$^F3 zM2)|MrmooOt}@ViiHX9y4jR27PqF?Dnhh`2(ru9Km|Yb&ps^xlnGqlH9vVkByKAGt zCnuhvWud8yl)>${oYb^0?meYZd)px=uV=Jxp)my2o|Oh(G>DF0ABl%R`Rm&ID2tfez8+DImJK62~J4~ z?i_9>!(ID<;0R~_l;ntuM`)Lc%1R2}KR;~!^HPFk zFSi_v=gNdg6gySgj~+%-XtBzg#|Mw2wLLl1Hdolo7dkpQAreK=YG>*VH1(jEyQpf6 z2^U+Moe+t_V)fj07nYn@HAy|C8Jsqvq8jY=$b~H7!(Cmh%sd)-b z6K~i2aWwhH?m6uzSdFV9LgZ?+IP$o)qJQ_G$#2{$^^Ome%5h4~N)BF{6DC#Qezcyh ziO#DMx#3w-Cwii3=yt2S5ls$sJLo^`6wFQu#7=acpPlR<=>+Gb1h1cH`)n_AnoJ7M zppMM(Xd1b#>V7meTHL9S6(3Pzvi!DaLEA_YU0hl*cN1E3H2VPM6|~l_R-q&PoMN{` zPJmbz51QOV!;I)LG{q{Fvx}6T8g|ys{Depp8%Jyhe#a>&NC`YS)xA+UMphF_F&RVS zgN>%y*6hB-kH*|hmVjfcE<{tmto8z$ZA}|+@Jc%{&cV(Jkti}O?)2*GDhettr|9Ee zvJ%`Z`ML7+aKK3$fMzFcCs$!!mc6Lnjn>%KxF8{NMmW~FKnPAovq9TyjGJW@xgeNl zyXao>v0zkW*?& za`2MS$=zvkf>W?0CHOv!f|x+1igWXAh4vb10Gj3u=ioO7O_qd5Z{3p@ElmlOy~-(d zU2?G7RX$%+vZCBx%|L5~R@OZ*K1S0@a35(!#9ocR-E}sMlq$EEwS{Qf%sB_S2i)Q1 zxoaX=|C;c%4bvVm2rb2ZR=LqBSe6oe58Zl66JKSPJ`9~1)MdQI;Ox2^{buyEHrh*?rv+)4qBo@ zCn9!?W5z0TeN1l^rvKAQ5lI-QshiP8zhj^y#HY*xHcu=Tjq3G zn;bD=IsI~WaR0YaDU2BG_3^>;R-8N+vv?YZ*38XQnBRoPlgwsG!MBuxcejB+72}k; zH92^hIk`}|Xj*~hMK)pgHkw_i+H`8Hx zLDQ+B4=Qw4Sa;pCit`$y+1-zD{sMO7I?Z)l0BnmJq2-6dN41R@;IMfkS5; z+F& zSD+0&Y56*u0?m!gVC8kT1==SFd!T6s?S`=&O-Dj$_vzH@XwBVTm0WN8VXq%j(Cmz< zSJTjv-5TWT2hjSW*?vUbdAfEtn%Z*n0u?tn&)=II?7qR0+^uaU7|=BRu8IExr{KP% z3Nb$S$Dh6cvL-0w9J{Y-$WN*fkZcU}k(3@Qg^#3kE=GLCL32-MA9MA1q&~kSRh}vE z6$42?y=cb|wh?Z^ZGiE+pVMV&m{gz&m~Z!Ux>QA7J^q)Z%6IeZBuj!z-0nE-9%$+e zejw8I1x|NAXOe2Fzn3qmDhGJ_nPdccgpcc^;Yit!0Q#IkUf>#xBvXbNvf^{P6hF#k ze=H;6#)4u_)YhsYxp=slE+J`qBoGT-{tX=s(6p5OUk3~dit59vfkrM_3YPp7-(W`g?W*JUI}_Ki^y1P=!}`24g)r!IKk_`besRDW1I2lX*z~`9gfDo@+cg7b*Ms z%Jii2uSM6u7E3Drmv{z}N?xZ7q=QriE08L9y{E78@^A9=HAn^LdL;jR8~Bn3H%sxP zlK4PNub+{s_c_nL$g?|5hTNXM z?O;gzk^iA^m}s3-|xxyk@`rA|G?8F<$*)m@s;tZXYiS4AgSc%o-V26 zmwd_oD~~^2ivQZP`xdDVedozzp1q{(j(hrX1&y1QwLWK%D&Y5cNhOQ(B|gIAC8d|} z^fO82m-O-_rH3L(=pwcvQtLX}Gx#N`;5ys0lhgoeBUND?kCzmGuE*E)_@Bs-PlO8A z^9t1W^ah@6h}1_?cIPA2fu8DHCxAb^Pb*ME`R3cw~Uwv*zrSXwefh141 z^?1o*&W@+*Yp#43c&wzXFZA@&rTC5>FRAWzMyh-lq^{a~dHJVHmFw;Cl1g6c>Bx{< z(EtclG{`d;>KRC?f?-~My2neN2S3--B{k8DJY7=tEk>&R5|95+>FyQ3WMC^;>Qx}A z3a>+|fn^>qDSo*pS9sER`I5?Ci4=8%r{Cz__yLF8~3(oUg2Z&)tt9V-`VM@m+D+p?5#~rW?$T`=89miTia&EEq<}V z%-lPxKla3mcMG33{U#JS8{ewtWWSZ>FXn7}tDlp(C(5b0C(R$>WbNtaRC_zh*@0Hl ziGI7E^Bh{<+iCuwQ-n5kZ)sPqMg+J{hT9c8~3OAW1ORC>)wxYhP2N@q)tkduy<3n3?FwNi8*^M^uBgXepnm^7d_=xd+%=pk+IEf!K zKD1RIr}xF}}l$53Pd}Jk0pevJa>EFLbt{Wq!{1K2P&^aWd|xm=w2PhS zFBl(M-WO^9RHq1S>X(e~%QSzQGwn;p_Z8zq>*h54it(W>`YMfgGVDg1_ci1DI?dn9 zDfpW4eZ%3o96H56n@M2jxav70Z!@>#)r1?NSc3; za};geQO0*P%|FChf0Xfk$N0WW^AB?de8>2{XMAYsPVjrihnD?)ntz0|4K4Er#`i;- zKf}rTf$<$*GKg%gXn|hq_9Z&OL;Y>Tu_)aiBv}~v03C4%E=tLU7 z(6{?Uzvc7%QOn!;)BF>b7x?>`1V8E_)LfJ3M?ZjWR{0?&nS&x$1|YfxAf}kY07U0v z5XVGJGpWTOj)>S;3}U)DDq>x6h#|!x^33|;5d9+{q9PzdW4dt0Y9VNQfOG=9=h8i04G)MM4yqA`w%A5V1js1!h_hqCqK$ zy&@KwhNU2OiC9z$VzJpRVqR&8cBLVfnu5|031uJ-iEvC}8HfWSR+WKRZVrlA83oZT z3c{GeD2UEwA&!YCG^u4Fj)>S;7UBkTRK&V+5JSpAtTOA%LG&*V5mg>ywHZ(zBC-O+ zb`fh#umZ$Z5!n?WZZX?LWS#|4^DKzlOx9Tt)ha^l5OIfzt_bm*h`fpr>rD}a|4vh< z5@LgyCUKY9DY4Nstcci*2TWl#h|bXv$3$#1snHNeL~M+Pc*q=Lo4HpI(jw}^RlAllV|*l7ytKqQ<4aY)2#Ch;7I z10q(P1M#{!C}QQg5Z%s&c+(V~3(>hQ#4!=OO=?|;BO*4|h1g?`ida_V10+rzA{TCzBUIDW@QtKc56b>Z%ttnh|WzRj)^#GQky~?5wWo;#P{Z?h;_{% zhBSjXX4W@@=pP3W6$f#`42Xk>Y!0zqgx>_4Lu?h1-5jEr*(M^h1w_pj5D_M;1w^%$ z5IaPaG|?>~o)eMR5+Z1dL`-c35!(u)w3*flqCsnjy&|Ga!`2YHL@a6zQO@iZF)tpX zT|7huQxFfqe?lf8L{v112@nTFtV)2WYz~T8nF!G>5u&OoOoZs%2I82AXp`Co;)sZi zZ6IRIQ4#BsAciDC)HLgpAo{n3h-wQ_%M54>5!nu6yNEg_*bZW=h-_`)=bCLIGLs={ zCPUOSS;-L9QXqDSsBfZEAf6MEmjcny6p5JH9wN3qL?bh;Jw$^J5PL<$nuZ-9c8OTj z0ivndEn?mU5bZ92h%*HjKqOoUaY#f9lXxM-0THV%glJ_BidfkZqFYCZcvILBqH`yI zS#!Ljzl%T7q;`Th(g}-=ov=tUM@6i=2x7=Z5bezRiy-=63=wrPM2Z=3F+^l%i0vXe zm|$m!ts=5JLtJRKiO5WasF@1U$z-KMRO;c8OTj6{3gPEn;3bh<4o|dYOW55DDEO4vFYv61zhj5V5K| z#HHq-h?PAcy7hqQX9|1p)ww6cF%bhyYEOtGA~yDf7-Wu$Sl0`W4ef=;hM4ueAo};l zBC0nQ!_0u*5RrW#wu?wN!9EaMMP&DZ7-6=F$h-ui<|PmrChHQ2YL`On5HZ?BUkdS@ zh`dW7vP_YPseK`0`$Ak{ruBtr&<|p-h-}laAH*&Zi~2!~GrL91>krYcKg0x6&>tdU z0K_2?xh8P{!~qej20%kv$aR8naDA<}iqw!ysmxtYHw< zE`!)1Vy=n44B|Nvd6z*Hm?9BV(;;HhAr_cv=@1QuL+ll?$TS=du}j3F;Sh_>ZV~fF zK(reHvD6fdfJhh#aY%$?5=TND5V2|`#By^`#L5haZW$296lOql9tCksM4?F?1#v{g z#!(PAn4==rjfNO98e)}MKN_NcCPY*w#A-7j6CyGTV!MbnCYS}WRYZ0c#4ToPDI`n5bI5mh^b>BV#h#iFw@3BG{}b7D`KN*m<_Q@ z#G-77O=h=vsRJsILT5qXm#icFD+sZ$_gr$D@5rcHrp zFco62h#jWkRES+77EOhC+3XfEZyH3qX%IV2!8C}3Dei%a5_Zi84$-r>^7-0AdZOGI0IsjIVxgZ9>kD5h`nZg9z_3{5K%KB-ZcYe zLhzp?@P~+fCK!U)Dk3`s@xIw6A~PSNWCB6>-Qkyar;Ih(*^xd}elwm^TZe-7JXDO~EXPgxL^>M0{xy zXG0tiv1&HN*XE#zm2)7v&4KvV6wZO@JQw1ah@&QTF2oTL8|OlNZ;pysHxFXSJcwgv z{XB^N1rSjM5GTxl0*J`@5ZgugO>jQMRuS3rA&Qx8A~F|1)LZ}&VX_uLR9gtKLqtgv zy%6F#5qS$Cf~H8s)I|`niy%syX^S8lTnn*RM3iZGEyOMni>`$zXLgI2w-}<`Vu%W+ zU@=6(5{N@0Dw@P45C=r8S^`no92Bu~DMYuW5LHd#Qi#siK^zkiZBnm;I3i->br3P; zsEBnA#1IFfrdjVm^j`)MwG5(`8L$i@ayi6y5p_&(ImA{G*~=l$HQPjFu7Iey0-~PD zS^-hbK5S>?HaqLDc z5>4tVh$A94u7XH1M@6i=31Y}i5bezRn;`nHhKO1Xkzxj{hKRfwV!MbACU`T%RuS1Z zLtJRKLHIkG3TynsLhmhjAX|@5^{T~PgRU>Ghw7Kj`|mjYWH0P%ed zOyr-=se?Hq)Fm@;E#ALmt-sR$U%$$m!MFLVnagft2!DT_(Ff7I)Ytv96(h!EHPQQ* z!@nU{+;rIBuju~&i*S}gW761(ll0%(9;oYbYFB~-; z?&6rZzS{dAaiPDeVXs{R;(t8i9}ido`9E^? zj~n*d4gFE`u5b+h8Ij$s?3<^d{x2@%{^68gVfZK6;s4*!>p!kDw4eVQ-1V~!%l&#u zxIB6Kp7?)pA^YC{-&gn#C>#1+z5W9l_S(h&zr|gDT*F?wA&&ji&$Hzn>{hIK3?EZr zPL~H^ggM#$H=KKsp$h(}3V&l~LVs1mUb`V*{(tMj-#FqQ4_N#E?{U9k=4_zt`|4Et zo_!7Ew{nr;!lvte{%WDzOdH|Dv&J@6TFvkWIL3{coZ-7O?bM~)pTvd!7mfTv8(uX0 zNu&FPhHVt)g#LG~K4WJ>|ARR`W5Zs%p?s|c{VyWPaA%Y*^AisBPvBUMr#JlPH1Z2= zgic#x|C#y2be|-J-zwbvq{4sg@xSKuR8gNltis<|U+AxD*lRcRqeJI^-x=+{XK?=?vbKLsV1~TEa{7M- zVfZg{cE9b^Q|CW|3;od*{>J)3`EvgsF_Pcdu-EP&`-e_{U*Z3wta*Hmzr_RZz2yJr zSd+ieA6M!~vf_#t9A9J}X%|RV>>WR0Kcc_oH(mI5@)Ooo$QJbZj$oiPzii`PqRIF> zubeL7^v+3rzW40(KA*}i!wZHyJH0fh8gOG@?-W#JdYx4?(8o^;(C*{Cca_OU?-~@R zcU8RSam74NFSgw0am783`%~Zha4Hkwak@o}pfdsAMJ3-s393@y~VN z1s+$?v#SDsA)Go^$>a1k;Tb@m${r^-=L5N1FI$woysTF#x&E*2k=4;FE0@pNUSYj= zsfx$d^0*pswczyRwYJBpvZJ2+>%j5P{prFFaGI`jJ-b?@4|;ZW;q=-{Uv2O)q~@@m zXIO{y18|zJ^E~bx(v`fz^*!!fxY8ciz~kz|mGQWS9#;?U71H{g?{Vj8{I8;Dt{ZtI zFLLv}?in`rxCU^$JiAz=9MTZ%@whn8?tHktr1fd;ag9iC@wgTq*BCBSBbWbMdSonW z_+7EB;8dmwxClcnpLox%Dd}f{<~0FM9%=@-{Bu7YJ-axx!zkGlx2nATql zQ0S4Vo?%P$>piZE$MJLelP}Ot^SIVLd4{~x{p73DdwX0GTw{E$0PW*(ZAsUMQ-EILaqUP4;1rCPdR#JT{mEi+eLXIP z^q0g3!5H%O^T_s);a35MUj^C$PH$!;P<(nVDF56ybiYeNb3F*DX}l2VrI?y)y)IN6 zSx2zbony`x&#n`kUiYi19_e!U|00wPKy#Df8D32KE{_}Kah=ii#&J!7`i$|&Zlo1-nuctT>rT2fF4SkN z$MqoH(pzHVJgz5PE4cQ^@o>uK?-{u7vh0A=&j;0TZ?KKDycU}1k$q4!61VprcL}<@ zqR(VF{^^fs_}WpkIxrPNjr0YrNvH$Uy|VpCCwSZpkLwSopwlPMav}E}^ct-`Gd;tB zq*b9B)9*Z~*Mop66qoOD3SPbKQ60F-bTnD^0n6ZEbM7zskW0^G*7PGvUaXH zzhmIaXswFsK=1L>VW-1QhuMDfbjQGgn%9t=1!jXeK(A9?43>cFOm?S0Oz3)&HvpYZ ztH8}*4baJ?ljv4(JGcX^1M@%um=6|!qqP1V_#XTKj)CJqL)UwwuK;5}HW&-Wf$<;* zaDau}mrY-a(hm#)L%}d`85j;mf(-C9b?WU(-9Zn~6SM{GKr(0#Islz^ZNPNwX8;B2 zRp4r{6lguOetoq;9p#=w;#^P{XtUQou6?@_k*iZpr&v``6CA;bI<5W%{ta{@eFXji z?gM&1yb0*llX_jH1N8pO@))Kd z^^VMIf!;$s4_N>ff-9+{BeD~?2wV*GBGB_eET{`A0-b(UKvhr;M1mj)mEudsppLyL zP!5y_6~I|QFI&;^_Zs*o&w5$phZ>+a*=3GgJ?40NI$ zVi2E#&;0B^he>=6z5tJa$H3#@N$?bS8tAp(cY+PT52l)SodbW>%k4+ zMlcGD23bI7)&MXNXtir~Yh`PS~~_`w{YeO$+@w(s6RcR)vh?s{}b^MPK!{cjR_ zt^1px2s{tAgN@*BunCj}Tj-q5W}R#+=zv}eoDB3T=M&(2@G0m6bY-I}nS+%32zKW0B7yp93#~S3xGY z99#jifv)XzEvLqG{if?RrN0D{LJZJ_h^|^nkk)0H5BVYTK!Bb9Ac>E_6nI^q{R8YL z-GvUMxt%fJbPb#ns!956Pz%%s+p)P1+y*v)yTBT76POHgfi6rYfw4fBQI`Q-C51az zjt=Y9xYApJuW9=mpuXITybs(Dw(5pcmp%`Ihrow)pey(Xh8uwj$ZO$7l^zK?0=-*T zS3bHhi3hqEQCUHDEkSGHw0vEhB!YJEZ9$u2)hv+&DQs0p^BI$Fs&~XYx;6Ti3V(n* z0=@+ry*l&_&=S2C%mE*OMPNVJ2i^mk*0+GBT1)o@Pz06$?Xi~vwMY{_@3F&)3rSx9 z=zPcxT#ZC)TWh=&hy)P;2WZOObO~fh5Co-xE}_l>Wq@p>Km|}9lmlf!MZoxdRX`V&;^L=1TF%pptGlo z?+GpeeLyeJ8$1Ud2V+1UsIir=abPUS23G*hspigf=ox4fI?klj^aCQu#uKy8QATNS((EC2;yK3E7AfkLnn7_b5?2M$;Y zmVm|JI*7hFN2r7v;>v{FJeCtsmtw3AOccqBN2p*1iHl6WkCce4vGO?3zq~H!C9aJhytZSDNqLJB1tz)y08&1E(R#FbS+;W zGy^)xYkKD%cbmuB0L9a7DTlPzAMtE(y*9=K$SI)C0PiQ2JcZ zk9xv&gk=+WUE`>Y^Yt_{g+yayBM;JZNVfxnKq}Bxm+n|}f!hKoPh1HghMW7?N z5VQiVfo_!L!FZrD(p4_OONZpzB%q39*dEA8ybP1U1>kh919}ROt!#vFIpuW%s!t6n zJpqgd-GI2T?KnLK$c7jLRLK?Kau9Cd^p0sXS)eP(1ebx)KqKx8Mgfg#FvtMIK{^-) zhJqnrAQ%ApgMQ#rf5<*p&?R5EcWI=H&1G(HryvYP=^et}N6 z#Up_#4`<3a+>kgym8dgHtFZL2y*j8m5B1eOgg|)~V)f-i) zvuQQ*25>!4VS`)=3c-y)d8%v`xCyKQ;St|Kx&pYxleZyn1-FCsU>&#%`~z$TcY{qJ zw1uzxfLz{$MjL}hpe{HE)B&|YO%MZWfa>5G?6-r5!9(Cdunjy79tAqAA3@%Xd;<9x zcpNDIN$?bSPV@gFi6Zbkc)`O5$oIfI;AQX<_$PQ3>;$iX*T5d|I@kp?x;Ma^U^jRR z>;=ky8@vnlgMHwA@Go#s^Zzx8ufPHDiA&mN5nqr#3_kVvkC7jNL!SN_vNZS{gl&|s zIwVy#oIdEKCF^tiod@cIbM>rP&rE6qxvdsbKV8#&E6Rs~X!L5J3aA7sg0nzfY$_o2 zET$wV0g40tfK$_wL|I)E#Q}LT0RKHw&nTjhK4j=92?g5`yN^9gAp+*;u59QZ`Q{@f7>F#Za{>$EF{{Kw(p6=a_bS4Ef2aSM&x`F0jJ=R?4 zL75(!=`tx6*%)X<8u?G>UYm~Wl&^-rAZ9%vqMY0;b5VQy3%ELjZfrabV$iq`~0qG6_?rl%+ zad<@Py(&}h)hQLaIh`*)&0l)pUqb_<0@ZR+O0#`G(mU(P!133XD7^JCwPyut#LOfW zBsbLZ`U&5cJ$PBbKQyjMT(c%|zPaYA^gwjiDJsvWWbxFzt=G1F0fU4lajly)bMGt4 zQuLKpl`a|Z_E#k<`IFnVYSOGpbKNz#|06Cl_wEzlmD`Y2-aoV@8F6N1dZ2Znj^DhJ z9;g;*>^C2!2bu@s{pRf9$gY0VdpO?%&FbNSXx{U+ZFrzXAU0q=CD;FksWSq0gh?6^ zXuSG$}KCS_zGy7dQm zt{Po#U1`^p#9nutZAZ&kd_Ry=nVhn5J3rX=RLh}mj$5=s33KhpKsEnRbH_+pK5X_- zQ_Mpp-6tIRquV5R|7Y$Gow0A#gvQvMO}PxZ_MX|eF%VtjFm@HF_ohlwIc4Y0xnK3- zq9zGGf20|jfg37Cnjba>s@13$X`R;K^LIYWt$O$YEb+2B-!#%RyBmwPSjgc|S0Df8 z*U8Zxv1ow>ZFY$??@^b(ugSZc&i%Y$rimSe!33tf3^i36Tc&x7HAgbkt8nkrG2j(y z(`EPbeVS!9jS94gd7+f;(nCLNsF2d~*3nd?nWjrC%`sJEd&HaI;zpyrWzrBaUoZ-V z_hQ)7C-?92?bs1c+%>k%_mv688s6=+_g1{PXXacv8a2X-v; zo$7PFN@i?Upjzt(Xhar|pOy6Wu&&o>Sv7AG$8`GQC@begy!Cyj?9Dqid(BgO+e+qP z%GL;Z7TudZu{QeoCo5smx=EazPVI-W@Xt3BF2{ARcpV++zj4n+->x3-8oD9X*R+$t zNJ3t1eKK<3rF+{xrie-;4)Bt#A>dUg*>gP=k2H^}+{&u%v%UPg#>L$`=F<^3QO=9R z`NqGVS$M@nT@koXi}$H3SiIkuZ?6b+t5GuAPUW(`yPjS9_LL2kPDV?mXmi<^K($uq zVNs3_WzW6F_d%C?S76biNegwT6*;t;-(bw))QiurGRiF*uR(V;8^_S6VHi}wpwWc2 zNxSc^v;_n0MyMaLvce=EvPV z{ooIEZ@wGH5L&jETd17<>jgwij%T;>ae?Xq`Y~o)pqhDUY@lUW@jqs2j$@bf z@?y?dq1(3GXmreDUW?QB^oYCvv$ERT;+Oz1pt%`4KG3p8)Y*0mysXXlC2m+Xk(V(uo$kk)bv@a$GdApQspRsj!RJL`qBqvav zP~MQkG0@$_T@|QOoFNY}M@399p{oL&&7|BwAKvpAb9JC|33uM-nXIb=!_Bmbf!6~` z=bE0ASPnDIHIo9}c!%Ili3d%s$$^7Z9GnukiWRnC3f($tHsuGR%_CC+)muH&z^>As zGsisq_Lh6@V$=kFlNPM^|2#7i$RQwiny9J7**g@~HuFaJi|+V%@xGQ6^%m;CO$Q8k zE#080oU*w-vt%mk`-E9PH87ekRhUN47MWqwuo-L?%wmKY=FwSn>6vMGVT0L6PT-s- z=7(v7eP$E8ri%A(e#wp!cRuX}gqDnHdL{j6(bTS~Qmgj8HLK+NEihn3siU8p-KQ$| z|6sOY9w^n!ym%!$U0wW3tmWVL;er0|AK!4ixW75;4bs=X*^24(cYs+uJ5afnn2CH5 z9{E?7{c_#j{jWZF%Jy2`H%K2gVn8V7KUepibz5(LeI7tB={_&+ff<^(F6&C>(L zYm}jDwJ@H(c2$?D{dRXaW!xyvyrQN%n_*bHO{cTTRKR$5bc?uB!NnIdI>x5a<(Qf? zaOBnIiW&4^DR$Z&YP|8op7?(E$*zUnxNkA5F=(|31D(q;b@m+Xc6R^eu7MkZ50S%e zp8wp~I%RjhcK>~z0~yjS<`7K;&Pz0H^H>0JiKcHJ3m_%Y%+KR$%AKi7CEWe1lleT4 z<(radXKO}@Avd&Jc-z=htyp8zXJ#OqgY@y4u2c3PY8>G%NxSBT1#9M99rf)+{3Xll(P1` zs#L-~!$z8=A=;g2Hb}VVP$jcF6sQ$bf_mhG3AgX{O|BoENj=`a^u4K=Z=<+#KI`if zGo8f~^L>ilrmlRn(tDLER6dHm7yRz(Y7tY|-gdd!lD-pO`}g>K4A{-pGpp9iAw(S-CdCfN~bS4UM2aTE2Ks#`k-iU)a~H@nVbJvWaAT; z`sy|_gJ%;6?h2_CA(&`%YnFI$%FDa)8oQPDt4GbgIn-8UHX_b2pJ505 z*zT27Ti@8g+EKN>I?;>+gz({l&9}!iolAWOcu^*^@TJL^=0yzn45zb#mU(`Ly|e%)hpOc0m8-cDYY~Q>3?>EO55g9+zQLY*#27#)VnzOeDs;yYeRZ*Qw zP3y(D*EV^6sg}&9KIk{W``N(IgOnXyZVc27dDS%Mp42AGFM zHyL0yUyqC*U=E{mHuSnBP_Q&L+ZvoqN2yO}PHHoKS7rCgJ;l5@EIb)3U9%%JOtv4aEcg>|Fe+gA5H z@5)rKR)&@^$Sk=I*LN6XPnM`EM+P2V_+H*A3)}a~5gahqxSDM{n&W>F4FxTJ-u+zx=WgS)w7mgJ_WU>zh2|9 zxBJ}t)U9qouE&3-U=_1*S)iu>CiB8FocZ&uy55WSoLI+o;?EmyFpk6dFZ4YByrb89IZWO*UMqWYUMtsP z+5VqBiFujYwC|K{zv82q>3#ZBo3rEYAt&IGx0-T>!+KWZ(*KVr`tWa^zN^g_rJgN1h>KF z+Jri!n=fb~rWY34O{>j*uJDom>)-c0K%Z^E623idMq3r7+!H)igOPf7A!3 zOl`>V8uafK@bk^%1RyUG--C$RNT~{xni|{a;~D2yKj4j-7<fHW?b@T*9Pc;&_vnNM4ZsXg}*zi~=sjSx^z5mV(p=j$$!>z+#`)JCp_pA5VD|Kjf!K5c%-s9!aYP;d)u0rh;r=G<(%DhYEF_SP*$NS$k z_=`GYdjHd{g3H5}r^UvS)!emFwxtcgDw>ru`rM6WZ@=Mf*M3}D93OjI#3$z1^|aV$ zq}>odO20dz-Nf5pKQ)}eChi7Ynr+e~+?lCToSiz)T(y>(!kdj-rw#g_tO47myHC|# zMPRwJ`s>EX7FpTcb7LU>WEk3zh#({wk*fQToMy^V!PEDr9~C?E#yj5(Qv}^)+TDb( z@f04(Pqt}xXV0HMmvMUIru}N}v%?c`%0-$$yWgBPALi{-#%?9{fN(FQyj>^eSeA{B z&86SUo;v&f*O)7`oWA*TQ3c8kn8zTvw!_AjjGPG)nR3wCtsj9T}4UscY@#D0E@3fJaY zhUfPu0Z}u~_t98;6M5w2{kwL&c%SaWbbhlP*j+AW?KpcB&uCqF&GLjc7h&Mt<83mx z+!|={+wSJQJE)ze^ldEbGu+L4SF9)R<_Fwn?}M(r4G)|!ccA|2+o2Ep+D$P2@dfv< zxNzUgblrQH`@`%w!p#kvCis)Wec&2yl9_P_yGY6uv-u7j(A^Z>fdlrNW7HTk zaEjd(t1KH(CU@?{7u|Bq9>4fz;?{9xm19EdFwZmhAY$s#x^Bjgjqz_De@@4@ydDxy z_9170=dm?-D0-&nv7X<;k4@bI?$f;E^g&Ovhfk>6u6^lp2~_H>_HJg?dLF&lzA|UE z?!*aaxb61B?F=`=OoIkfy{#F0Cxf?*npJlOu4wi1)@OK_?A-$|Fzq(rjql9_gq{%H zz9G;x5HZ7iwSnV);0(L2AA0p)!`J2Bt7m}TGmlK(hRmLO+zgVKZYJGDEtw|eA6&FP zaTlBCSt0YzT^t>0rou+X_-4rZ`DoNR_f>9x^WlL11&=wyLvk*HbOL(o_5a&HhS_qJ zy(XMoXGh+`4{IHA=h~g37tNNtamwrFLqzL$uv1J7e!B62=)C&fvGb;~B)`E{o}2|E zmu-3Yt)h3mXknAAdX1_50H-3+$djMUO}Nv2K3Ao9OMWxL+_x#vE#^N^g+xP5!x_jV9dQ}2j)IGj?k;P+^$7MI&e2r+eZ=s!~4R!jj z{ciS&y0q&p#ZSr6M)Ap!;d_TwjMPI`Z~f0OL++u5Tg}pYaQ7Co0TJ`kwKgo~?td(& zSdEF)TOU&kbIp?F7n3wLunZ=gci}vgMO}k!x_@Mg( z(L{IdZ2|tQUe)_p{R!9cVp3-F$F+I2gewoLZp-dXuh(k0wVxl29dhC?54qJcd-mo& zeCdX}1LZz<9Y;v(rf1dt404QF^eAG6*(q&~dHhj^+H#HA0~@d_rW$`jr`%)g_0O0g z5}9VhW7xi7K6?PM-@N-6d+^t0!d6NYUv3s|VP*@kqDJd&juF;|E9l&O&$d&NlZn+VKR93^5fRM0h_b8*O5q zL_27PJxKU}NhSIL$JX|%UrO^m{56&67KT+`W`()?LH6A$E9`_%-S%~SM4QwNSZGyq zEV@-h$CSZ9H&+QOVwcr<@~k3$2;R&-G`9x)9ir5S2+B<+?;#p~(3pq#(cGr%?W$^e zNAhF;nmt_42fRI~{S7M~4&>_*%GD3k{>?Yq&0^)U_D}tDYyWB(wz5B=+izZeIMAqW z+$#HIasg65dPx6&)m=?UltCDFM>9pMZ4*OR%*{MxZHBtw%?f|8gFm!F&|%roUA{Dn zb~iQqS;Rx+p+kq)A4=3M#4=M5E7L4j_s3n$N+Hz9JVanp;h{pTXTI-SiLgV*PQ%O0 zH#6`1KJPm-&kVcIHKVx=FZ32*oFwN8 z!pkkxG6_mm~_YFZJ zfMOK?1Af%U+ofZwN|86W(r<2u)sVzEaL!Nx0H(W+1{_(YH<`V~+6<&Q(DULARR?Oe zqeinac&LH}=ZKCAaY(K{Xqj>zTPx;7JLg#8%q78=P(Umjl^5l3+( zEpGRPtFzXgd~cZ8PRF~I6~c`$^itb9C5mEmeof`sy~{_K0=Yw6gr%v=*(Zgs?~>Wc z?g3vVDRJsg_bL?)=r9@s2Y#{Yk=|a@t?c-wR0It5cfYRSTNS=_e@wq;d+>G%lNKrD z)S%G26z@fNZQzaUbq~nSvC}Ir3?|Kx62_TI6&Tt+!NgT;c#^WlnVyAfsA-(V^YXD= z2IL=*jRktix9)4bt0RR)cMOnul3TRa3d)kaSpo76$hXT*)SGe-$Mwqf)&B0)+Nuw_ zCy+V*YEI8&y)&mqRp*i(UD5gkx;+qYp;DX^;Vzv=kxvizQ5le-4Cz1*7aJ~I$i5pYp=cM z-sADp5x;&PvAjua_4^hkS9pE*H-qMV`Pi51?@YOLY3In2sgZZj@AK0q5x=(j^s8CL zeEM`Amzw zWS&Oy4jHO=53&MsJ5oKI1F4a0@c16IrV2v*#I)QQYOyr6sUxlUQ3)A^j7Bb}TzT0e z)#1eymOjzrGjigpOrDbI^L4<8pfK*@FkuUul@+CvH zz30{Qx+nJ_RbQx{Ki7X{RiBT41J5^*!?4tm=}~t2rsYh^7!9|in&)LCBgy}Wd_EoWLbd}elrn;(jPg?@V4WZ(3u%Jn%Vi^n$aQrq?+bJC<- zkw4e5BWzpOx;Ax8R`z802^rJe%Gx-o#gjTuuV)=S2`PVc#zH+Elbw~5Gc(86*sC=o zr%kKT8Pj|<8`$v^Ju`DA`FxK$M~YX?d!nIbL&1zP#K>x~ZtF z6=$6=4ylSqXQz%yW3CT*eBCB?xYLo!XBgurWsUZIi>~;o>Xi#ppWINaxm`?OH1qkG z)BKl_HIbW;wU9R=Yy0wi`ROETK=kwsnj)F({7Oiz#xqTAynKu#Nb+AqD*th0S>yvq zjW{DKJ8f)6j&DXAJD>?jd2Xa9dm|Mw9g*dbW78&2on8jt#6gsYsP4=wQM1~w3AWmA zJn3s^1L{bE^F)az147HBX$RX%XxSZZZ)<%Asop$?lv_jc5p?x-6H>iehm_kMbJ~`y zS?94N+l5U?^=(uq+t;g*s_jfCXLiZNywF_z_k8?6ck$S)X=#&Z_V(ocQnx zvGaS_j?eaF7E)bg3f%!U=w$~O9)^l2HBa(r)j`Y8&i+w%zE3dpI>b9DFX!M557Ia!m( zWlT!jfUXENNLiPql$K0r?>gJl=0~Skr{y9QxD$}-X8bU_N8XK$KtGFAto0afhf~%o zR~4S_=j9(FLk;}^A$e@5m7iU0H>H#jc8h-tT^$>rHD#>))D2$2ospB8ot>KNYvPp; z8)ZAf-jI_vO`A_#X6h85ua4J|Vst=$YsU4Wg4X`Htn9e7)G_HNudx}^kX10Kje$nA zX^g$VCeW-p*bu1^S4GN?Wj+3O>{M|%%BiDuQtgOOA$8R_;>kCV%HM&kmPg`yMy!h3 zL8yXt$VlWuq^|6_NOgpfWT%Z!o0X0(`+-PxbZ3UuD^9Q;=>{*?H$y5A%6N8X8JIf$ z2~zbPMCQrhd2ht`Bh}N}J%eJIb_9#j)#DUoG_o^N9ekILS47@J;A;2G%88qt%5`Hx zmTiDte++H=Lf4njb)?93BhT&W2nwjjSyQ+w^7+1}?fIiq?a1y!s-q*a?Ff4#<$+MJ zeu%w1@s?MP3s+j)*wMa8Sz}Xk(pkyy3c?SG2eq>wDG%(L#!m&&y$~A7D(6UW!~vtMV}dO`MHYX!=sVv z`179qBVvmzq6( zN@{jaT5%eXi?`ft$ElQob zFCaCtmdLWmru=h$w6VU2$X7?wa;8pC!^ygMrV+gB;N^+ox7!Y_K-ZLYUuX5YPUi{@ z@`l}E{oV;#jojH-RYhKntby!|lpp78u&2a$boHVEQoRb@y_Q9ndrmmET*%KMdq(Q^ zX3Sl7zD^+7fAjJ`_K3YmH9RIKCoN};&-V&H4;ze@;KVd+?_cH=HcSdWg`P;RTB zeBjsO&al`-|6r#eHYu<_%y}Or(uuk(DR@;0pRYG|ekV0H-haPS0QVK#K$oLS{tix5 zqom*^;XYp{xMJ#la3EUO^V;g7d4YGsoobB}gK^BK3izGcjpKuvXc~Q(vyskjLhIlh zYS2FLY0#+_hjA%87S}n!MQFBm+7Ik6<-Cu{AcE?0Y6y3IY@C!PNr4YaJ8PRH2CES1 z8f!6mB6t;Af3)Ii%73?$(ljY}2CfI3J3+xN%ZB zg6ieOG;bexy1es#i^Q<9>};1iF$+34DJ_$N%h0^}i;EAwraX$e-VduoWcGK1XAUX# z*6$o?7VqET6tqeT`w=cd&J8wZM^NtruFI}*Qd%bkZ-r|CS5n>y>g=HMIQ8m;aFh;i z){&bNic4{)=s+cBZJWg4WtDA*g3d+)CEbaNPYOQZakz&${>sye%dP%~P9YX!*v8cW z(VdYH?kOc*zrTm3=88GBN8sCpq~HWrm>8mC^W;jvp zlKe}Z6n?I83fd(FU#aHvwI`pTZW`|obE4WO1-rAc^@DSr5nP4V*|pF#zl)}Tz>V}g z#);~HqpCAMaBlm7FRD9hJ0u1p*$W!Gm27MiAMA?e^*1&?n2)A73UgOTAzDY*Qj=Y! z2IqZeM7#FE;iQsWUJb8Bvz?;h;4x40D{%byBrz#Cx26p(JHz)oDM?AekKxoc?ApZ# zW7)qnqYR8*3`bKavh3pHgNxAQh*GZhHd-5-Hz*;zwlyS5+r;}9I#HdIf=|GyDk^4p z-=N8yWt9-`uj3SAaaElVSG#?@f3cHtc~aO*aLwGo{Z2}=P2}Fdt1VyIXefWiO9lqxQ-TLjfh4@Cc z(UR^A-GQd=+0c3Of=2fO#Cdm{hc*aJb=npTnszuGK-j*4*2A^bCUluo*gGkh6=!=y z&6mdqx1d>vDMY_SQ;+O6(41)1R58Ly3E?Ojh~1}mqRA=6-OipuQ?Y>CXsf2y($4;5 zv_vX7gsDY1(XGp7J!?LItL$EOJg#2Tj;Gk)9E#;)Pjt{K)YLtsy$GptEsSUBypan`kIA50{r zA=$CdKqJJ|g{JYrm(b*E>!q@cNgX28xm->~(~Q|=x*JX1V>e)qN--s}wCi9Pn)X9x zb9zEJih9TRO@hC(Gi*d+usBOo26&-SyuX!GI3g*S3P%LfE$zh)nwApB!OP+U?_KV! z9hn$bvkPrH>qfQ@8$oKYn|g%QFgF!JDhd=fA~?8l4o}gRT)+C;o^gXRv@<#0Oj8L8e4~ zZyz6AhQ`D))+4>+gRh*|w568l>GKimSSn!qqN!X3H;P&kkqY$6(!!>pb#TEYsTgLnIov3k1!587|I@8s> zM4$85IIc52(1>q!n?31zG!2(6FoEL-nxe!#egtdu4Nayj`=Y5%_k13_9!<5l*RH^; zeVt+HoXj{us4G7AA~Oz+S@H_phh~G5a14CX&l#4H7_8Rcy4Q|msHc^1d$tEnYd754 zI4!|RnUEA5JisnSyO*p+Q)5Bb{U3T7fyZ)=WZToI?4H~MO_p|{&PTJ|Qhe`4BNjA% zJk|C}+k(~RqbXFG{G|!uD2fDo3XEs(RF-?f46Z?wGi=O!fo8qH8VJ@I6sjaOFQGUJ zi??-q|4Juia#HXObd8bK#TtzoY`u@hHj#>^COB=dY#v5qYs)X~?xVk;X)3h_g% zYuTh2Q5Kq3GusQbJc*{R5mdPSryr4$hi2h+t4t&tbOncQgS0aqpElH z`B=pJBhPEv3fElF=-w}Agq+H0#~NX;WprUfO=veX;z-4~d0B-f zm*O^7?5k*U4drN{(x}iFHR54t3U+4nx`goain5+ZlkbTwj_3g=YGx9fU&_p+;LW4$ zJh+QKuxGUM{>;R%z!>c>BQm(ZCS|>-YMw$P_S8CB=dtJeLLkgR^Sn>bUO-cj*vn)@ zT2ad@&=SwrvH;EVMAP`N18CixLtMqGuv-u@s$p(I_<2Pe)$?e!$dLvK;p6Q=!+K-_ znr&Aue-zEOw6VKuNc5=lE#t#_rE6Q|jz4%iDLX{X?k8wk^49s)GD6i6rT(!_%G{*j zK9A!9*ES)1LTIkE70pEBQlS;b&bte(gKZFR{e;FMQ}bv&Ct7b&3EB!Yb%6^Vo_r6@ zmQ#l+PAXb%Jeu{&5w?uoXkOPDZH3IxCZR|fgeE817B-?0IvN&r{*LCkVo^fz$wa9; z*)Nb{-qc0zx*B8=c+R@|?Sr|b)QtQ1A@~ehi}Tt^G{v}kxeq2yv2kX%m^?HEt6lNC z(BvGu;hga_&O@AXI!+B;_tm}0XmS9zP6OiuFHUua%})%L&km6)pkJN$cTIGydJe4@ zn%nVU=^Q)q;!f(Kgm4tO-R&PwXbKl31&-!8)ox7m*K|^DObQN~X1i=pHpkHH4C-px zXu2IZ!)cWej-n>1HiIV7G;U5DT%XF#upK}vNC-!fgX{y9C1`D2EwwAf(6l9T0bp%+ zo@pJ#cFCCLpeb5ufUV>uH0!R^9trx@gBKEU@&3L};i9DAb#U^oJss^tYl6lcjf@X| zfu=@rNJ2t*ZYZP`U00#0cVwa6h-TZ=HoON-Lu4~pnoxW;hR(X1I)szx3d>y&cTsE5 z?A_Iy9&f!~X-;TYRJx zW7jHJmK02y8ycBzi>EtLw_zGyNeLjaJruJcFXib?=D7j-ugdR$8^|ZQb@%dlgzMtkAd(TlF_^R@ zH0`OI5}c^plY-yE$jLSk>fLOswL`rcO<_SJt>eRXNOPY^{6y+TwK3IR5wXftJAy6ie<=(D^b-sm-MKKh1lvIEB31-4_ zyDQNp=Jt9tjjD|6g@2*-MyqJ=YFGGt$!P2zeGZ|XJT06RrJP*iEkfF zR|*q`cw2n%2wD#`8fTY{U2P9WtP^&%6g1CrU_!X35XOz;0|!<+!|qNDMw|01mF;x^ zT6gMVUl@=Oj$#+8ww?FTvfV75xO%R!esu$r$7qH3(9bnawR;nTKa(YA*pq3i0$abk zKEv|S5}iZ0bqH6&8nHaiqA9>!qhPDGwupV)H66_c>5;w(#Zh{=GZl859hu#;I-_Zw z5Ik&1)1AW2Nx}Q!G!`s6$A_In;};-25p8h0wGFyAuVbCUElFVy!?kk`4QwAQvCa;f zy9jpIerQ^a?gb`n5t@AtUP$Wvittx(QtnR*PQJs|%f2=;Asj^pHX`0f>yKt9<+Amm z2R&N;H=^|;&u%X7pbc_0yv(z$sI5ssv%%*ZVC#yF4;lvr%0o%PiuauNE7NoZn!LezgbwYBt;8IUE@MJ@zK9YW?&)%we#Yu4lajm)T`J}UVxu3t2s-1B66$9}g z0NMh5B$baF-A_?j8m1diJ`}E+KSibN2wnH{@3PT_ z3>gxz?&o4M;BtBDdZRp1#rNyUeHl6Wy*MLF*bmfYe7)6>SB@oQPdE3U^2YJAra{0Zr<& zK%ZxU@?QY@{GGgn{C8X{D^>2Grx%rKr!W+SB;1drruPs~!NWkG!|uOxCnarl1l1@0?E>zEF*=Fq>>SyUR0KckMZ~-(mJXLp?WT6_&?_5 zze~M_C6#R8=?(neL|q`hZ`t*KmhNNGkalKh&|E9)B^Z{3pD8Nk!uGNOkN* zr1D?(@+GD3^YjanP#5=#mm#T&-a@LtzdT-26~E`{lA7obJpE!)Ss(I4^?c;X!$_6; z45|9QbhjEMJcDCMeg00$h2MGhCp~*fEty}DqR#L`4g7{wg8}uzliIyYdU{?cQB=yH z38<^D;k=l!gq z?<&u3kSF!^D*pL~dU}c!K9cI#)gC_rsez0^s=o1F{sdPt|Ct^!1*wmuD#-EV3{U1F z`RAL%4^^b4BY6W-_IguHA4&0xks8<%Pu}e1ODefcytdj~NT`Bakt(>_(+gyPzSh&% zAr+kWAo=Ip#1Bo?15!Mx=057e&orIk@`rAKkVs} z^1v~V|H_kJd--1nXkY2$9wDjZcl=OACp^BW6o1mQ`vIvA{p`upp1q{(e)V)oCC_^L zd8y@~j|dew=joEgoTcyBvq(T~@VS^&sbXHfr1auQooUM<75kOF{J)c$plY6-q&iZ) z7QbbdYvl2gio2$sUR27yna4}2^%h7` zt@)w+Hc~t(J>KIJis{R+d`P$j+Ixnsbk4n7wSuZk^jJy1b9_$&_ja)>nsU1%bwKaq zSrwIPtgpvQDmlQ@B{ir)NYyh0sjOl8;YpPr?&Xj0cx0Y?M|*=uNNN%ndAg)3T!K^u zH+%g5M5_E!ue_x6Ws0JJ)8}B-o>Fi1XvZ^?6u%NF%6R%3DSV1b*%ipn*?q8TP@P^c zYiH|A_GaiFuk5{E+5be!bc<(SRBD|+?D0jVT74A$QsnzyJ~Ges>0t#C zsuq`vf1Z{6^Q?qu_~%&(PWbm{C46f~pZ~RIB~Hrw)13MrBs-BGL^yLlNcI_&@rf}ag>!as{}GCxb^ zb=F;IJJDi3Pv$Gb8J`bv(m#)I_M_EyqP`g7M1H~ezDV}hb@rj{MT`3~*v$~L-^f{XjPV^~d}wh_+piekSB&qgWPelV7}`;^ zK3^yMn>z(xGrq4GA6iSN=QoV+8^-rdvcI)+8toL?h~vrrcxTIT#&?|YeVgoW>!f_k z_`YR)XziWgcZ}~l#`j&azoWAYZ6{jHiDZA0lW~IaonU-uot>zYjPE4lJDKe7;_O4) zix&5NvcH>?`#t0Pp7Ehw;l%#H_I{ujKPj(jl$oPI_d}w`~w!F}h z@Dt+^H6e}Gf)GvoW2@u6Mm^!$bK{lfTuN%jwNPNSVd8*wVxKg8K` zit(Lde5aHBDNf31#&??Wp$&I}zcRjG8Q-tT{t?bDw4G=%XOjJ+oQyM!?+oKZOLd~o zGQP8n?`*PvjI$4IFIwEWWPh5Idyes)V|-}io!H+P-*1fXw`6~YQ;2pDt>f>>{)x__ z-x=TUj1MhyRa^g%RSEuxRqOo8{;XBU(2n{eOdmh$R8!#h4>4=}5a&eXn4STM9s!80 z0f_15w1`t8MihgXX|@!D*jx-EqBumZNhuC7v^d0W5pzs13?e)XA~OtPuGuAGr-+ym z5P2q}1VnlXi2WkwnW&NwktHGKmV~&$>=Ut9L|iz;0+SmKF)JM6kcgX1Y!ISh5MpT% zVv#8naZp6ZQV>hbqEZkGOFpZgQ2`>o0>pk18%@+D5RsQa%)JESF0)U>UJ-E>A?`7`6(MF-gg7K( zlZmYa(XbN4(n=6pOd*8-KGVE1;(oJ8;sJ9+VykIe1+mSnka*A>lX%EEm{kMf zkcgK}Y)y!UH6fPPgxF^aMI02-u@=OuW>GDOg|#4#i`Z}4)`m!^4Y95^#2eO%~z z53yUs2PSwaMEIo;nU_KwGP^|V6cN(^;$xH103y8s#C{P+OjJXN$c7Me8$x_)_KDal zA}$u}5FasF^2m%oIv|Wtulad~FsX%)&+(A8&;5anrUjL_%YT zb&VmuGshtOCrsx!#7R>i@x3`I@q_8v1o5NUDDjgyjWDO0P;^97ivD7@G=Dd*c zM^}igT_IYV(;`lZ7|{(P-fZaxvALVStSQ^w-^<_Dq;!WE+8vAC-LYtIf>%I~$UJ!>w zTw!99AsTAhmvSQT_cVnf4vOg58zR{(>J725H^gxfeN5Xv5D9%C*7bqtXO4+DDxyzc zhykXcFT|R@5a&c(X?pg9=+O^iYd?rV=Cp`YB1ZIw7-F{c=f~#$5D^0)QcTJKh@k@@ zc8eHpf&(GK2ja2G1M%1hvrELzfmp;`iNz?BaV13hl@R+yq?)L!AR@1Vn0pn(7_(2r zUJ-GFAks|kAc$FmAP$KbZ(;{SG#m`EbTCAQDHL%~M8_cz6V0L_5DSMu92b#k+75+C z7z(j&C`6VyCgP}wJ}D4WO+gC8niPn0B63X6VGuorL2MlcG2NUNaZ1F9;Se*;mf;Ya zheJeM4Uub7u7((THN=!Z5M2&)o z90f6V6vPc?pNPF8;;w;MU~;d4m~{=rArUv3*i?vysSrz3Ar_fJ5eG$d91XF=EE)~5 za5Thm5lc^=#Tf}W9m<|!14w0D-vCixgu~S4$2E=-kkpYpO z0kL1iMiVswB60%6+zAkOnSCPmiin#CagWKJ2r+9S#32!zOzb3xhLa$cPJ-BC3Pl_g z(J>R^ezPbOVqqr4aS>Zh+sP0KlOfhkhIr5%6LC~TpDc*&rXUMqO%}vC5s#RjQy_Xw zf!I0)Vuv{`;*^LHQz0HVTc$#6o(d6>4YA9lWJ3(ihS)9QNfXS02+x7Y%z@Z#c8S<2 zB4!%IGbUpiMEW#{{UV+-QPUwJr$fx04)KE7Ct|ONxET;Hn%o%>vt~dX67jN$oe9x! zCdAU25c^D_h=U?J&VqQ=ESd$ea2CXI5&KQsT!@5Rh;_LTZV%a=9?fQ7DAjh zDGMQnE`-=E;*1F{f(Ty(k+}%soY^H}r-+!vI)M1igvC05EQZ++qXWniOd^*+%v}Oe z%Xt)$&=~9TGDHL%~M8{cennDo=MRY8HXk-=@ zKrAePI4&a2v|S64uohz7T8O6Rn24hy`rHQ5+!WjfvF0|2b0S)rp0`8vxE*5a?GUZa zX%VMx=ju6f9aqnIvt=E`=5<&^+<`?~lX3^d&^sV@i)e3x>mkC|Lu9Up=xBC{*eN1r z14NR^*Z`5f0b;+1&L(OjMC3+@xf>z6n0*_$wNGo($eGflva`HF@$#Ynao~1-4Zv4> z@_EO!DVeoA#K_3Bx{KFDZK+Vk|E}y*Kj=1e)x9({zDaD69{le%bYW!wX}!_THBB43 zu9`l1+H_tA^3ADzQCF$L3Nox0`Wy5M>bhU}%-2_^LVu6`IXJ!yY5m-(euO*ht`Gbd z!(Q5;B)7T|TX{bAcizFQ{CvJL#JBzPmm&F9|DPxHKOOcT?QUrhRzCE%+gI{79^cpc z?^GP+j`FkZ|HUXTJ~>rg=j%@u>#=M9&UScC^ZJ$aY}TiD9g9ZnA3J{WDvR1wa;vXF zP4ZgDp7u|Py34?up7J^A_U{;v&Y0X(X|?T^;tvaFO`4JBOG_>?ssDA8MVr5Pn)MK+ z!RbqOMZQbtrpx>v4*23N{%`d9|8^an>W905)#|zbQDFVwbVNlrR?GiKQ`f0}88^Bn zcz+-H@DlrHi~qDW|986Y?n~uEe?NDd^Oqage^RNvCd_~J|H$bT-68&MkGzXQ`OshQ z4c_A|{Xbr!{*x|Sn@;t^9)I8s|Gy@eI@M8oG8<%9^JHQmG4HqE?MK|tTnYK-ev>w& z%UaNDQu>_oZ1mk4y=tY;X*k(vy;c^%M=!ppE`2FhuXpNm7DDCp&D%&3`hMTbucOmIWO=t`r>q+!r4bJx*`sC@HTbkUo?lD_cFTzQ@&s z8|!)SQp@G}YC(4M$Oe!qQyW|kr*}|-~A-O$w~Siy8ddp`)ThPHbCQ#M!2629;a_)kM=k%9`!XAj02j(L^%HO7P8Nm z4)p2d*>Mu_WdPZA_Usyy9t!m7qL-IcVH{AU`gHZUCZvC(tBQ$k9@kU`%!9b@9@mU? z=oOADJgzyMUY5~Z_dxPbe?7qW0SQf4FQnSdYsnA2sHFKyMrs+h0{fNAXP{@u(Z_vn zvljA7k84AE6OemDuZ6{v-t2LqSHu#~vw=K0*elzX^c1asje3Yjwj(_i$d!7ZjDPy; zy1q$3t{mpsbs#+o=ri2oI+9j7dGczHOC()VQOjq9$0d;ty~r|B?;UBNoj`M3Emw}h zPyx~z==~zOa3SU9G6}jRjdR!0EngV?$d0bD@ngV??J+2q&7L@IVoa}MQq*b4|ERX9= zTCeL|)}4gjw38S5c%&+s>T#VEjeNrUnql1oi)-uEjSs+w;1Ku-=#`m~U=+By3CV|O7JHK8d9ssTcDPR~F4z30x!8ITi zJV$+c7pE6U2E9Q7Xbai_T`@X>M132%4T+f)m<8s5x!^jm6lhgybJb?5)u_c+6Vw8= zL0zDItUR$>0bBwq0-cP1#L2osd;&fNpMx*JHn1J&-JNQA{D=m3g1f-oK=0Ij3x20! zO}8K1PjVf3cL06qWi8N)Ma#e~K<_e@!LB`0?>j99dO3JLasgNfW>7~bWM^OFT0>wcXC;@aW)3qxIN`o>W0+a=M>rmIQH^7@fm#MeGJK$ZQ zOV2CdMeq`MIl%nyC9x0e0lUG|;2H1$&~-^K_I?Gv2H$|=;9Kwncna(W&wyvab3orG zz87o)=NR!cpPKIfnSE0rZ-cUcEXAI)IKK2Gjt&i|Et)Vux^|Ue7E>hXPo%D3DD-E7<}H)pQ9<#3MDqNA#q>=5~c#1p0wKK-VmS3x0(E3FwC4G&l@C1#f}3!Hb&Pmq_daTflwbeozW* zrCnVgb^W`Q4(O%Nc0ljmo&mpruRuT09}EDWQ|=3(_xs-g?}C@XUQin7M54=I8=xal zbK3Xmi>vRG8BcmCaxHk4bUadr^41_1eKxoj=!jd2j_jx0>p(}__rMF_X>2bg|2Ob8 z=z`uATn;kPW05Z)_kg`1?{$8R1>?YYkO6+9avjd9B6SVbMLR(H1Pz=8N5E&`bMOVo zAzufwZQvu)-D%_sufYn)OF%_X2~-AEKvfXw_nEAIfeLxMD6}1H1e?HSunw#RI&ftH z9ZjZ!2|(wltHEWUG#%Azks8`FuE1(v0jL zsFSA~1O=+D_`iUkf%+u}=y>oWSPB+^Pr=RL2sjKr2Aa7-@G6j_UICiobfA?;)xOI? zXP_8VEGoVghs1cEdq-Llh4E^^`P4O~!$1iT4oZPYPyv(%$}a=TgL0rOhya&>s-QBc z2r2>PR{?QA+k$4VE~pJ^(Y||h&{0AGu46@%m)5Z&2GjwupeASt6zKIqJs{f#KzmJd z5So=F(utrw(5lm*+5ydqW=D%o=?=87KLYOS1p0zLK(6Wyx`SlU3v>ZJfw-=q8|VS9 z@O1J0z?EPi=nn>fm%+1OBG4*+983n8U=k=ZfghSf&6hbaFwiLP_dz7bf$<;>j0IzW zVqcr=FfbGh0fRlH{3)&cqIoK#vPH|KqHDvx28;xwT!}|UleorJY$d9^XhC&Eol_^( zi1O7ysDnjqHE@kl!oKRWj70`yz=J*)v1o$0Imn~fb8d( z(>ibH2=Ez@KVCyBfL=oC82K1j25ts7f(76Pa6Pyd=-~Y-_Blu$taRZlj{Z9!sVjS) zdvZUE5)M;REbxQkpcn{%Fi-;M>Z_}8d7yhBoo#e#(|IHUlmlgfcyZN$BC7%NQqUAs z2hl)R@M=0V$v~HKUB-1ej{;oIU0z53P$dmXhbq$TfGVg7bX!m#=(eC1s0ZqL={m^4 z)E}xNBz0TS7>rbC=pd7bY=pebgLK`i=>ByW&=p$uwar0G&;lq=Tw8Dj=nlF89h5Ey zt$+?(?SVYl2B?g5mD8atPl{aI4yYm->ZBIpW!MpP{u9>;JrT%OHbSVJ^11@mrv{at z0kK}-ayWCF+lp$3X}OruE$$zVJf1;zo5co0Yf8r5(x7Nmk}z(_Cx zTn$pdP%s1x*8E>Zq7Uc^LcQxnIy4G3rXF7jG@`y>AQ%Ap12wMO2jyv`p%KeoY1O69 zU9A3gd0uA7P@|QxbWK4h9f|=>gWRqW$|l7BIjyqtj`&cgrHp@zf>szi+{twyAW>=mS{LmtZn@`@Uljih2raZtoQ?|dbiSIwm|l`dlB%SM-* zXM<^AI?xrXCdef{3+Re96B*j9XOPyF>dzbYJoxLtTvsyx`6M(A$r#T^E&+?dBCrtL z1eSsw;1SRnyYn^59NS-i^Eq+ygd)P2hg89XtrOg1l|~cnCZS zgK{XH!qQHyTKLwryPk>!uC)fj?2D(`9My^LbhkOP+3zYvn zctL;U?qw3Mg1ulLc*Vn~$dADx@H*HJ-UbK2o8S%b7I+{03%mz3x_7|4;2I~I=X@F}6sHM4$% z=IBXIDWIo05uhBnl>D;DaO~>=9ce0|tMf`%LBi*qzg%h*di0}5K>GDrG^h#joGNbs&bya%L-5z$W&Z!)bnl&=iIK}(>AZ7qPx%0`?#BpKrK5>PakZIG(0HBw70 z9$60A0oe{bMOrS`dTxu}9)v1VB&*TOK@td69tuJYEL68f9-5-g0q$8lK@{zAXhiD0 zDpT*(DHSR|iXVL1z54i!)WEoasgo6`Y?|H_2;*zh6~+dl{MVXB2y^S$K&$FHzL)m- z@~hn2?o`#L+wKYYN49CwtO-luL!bF%Y@kKpOP{HdhCJ&tz0v}`qqUS(;_c-l20gnZ zXAdP>*%Fn__Ow9Dz#+f+f&$Su(hnIfSwFjF%kA%ET`;U@T8;~}jMnd6Dq`?ZdiCF* z8uC^l2Cb>7Ik#nI-nc-Yz*oi0o8tn#13wirHO2=bqkDw8PaN`p98`1gv(H>tiUKW~ zG*@R=ngQb(*#l-GBKkS(%24st8ygh7>^r<0yXM$6Z|*B=){m#<4JB;X5_-MzZHwEg z%?kKiHK8UdnrNmzL`8GV5{c!eb~<8%c?PZe_L8>05mT3SsrKp3yvY{w)t-`ON_rqN z@J>myEF%zE{bMX6=s@qrmp*dWrL|jl9i@S9N}8u-`D;mY_4Yudzl1qS3jvRgjtsI> zP;kJf1M<2Ccm7Jj*6KOS+$3cLqT0+UWnJ*(+50ObwYqye1}&`%qVP2ro%|d62kXqb zW9I$jXkJ>d+uL6GTbWH6ftHNz18R!yfxY_pZE@cNFMm5^8}`HJr&Jw6Y>4)6;0$MBKp0GruQVm>M%j5 za5*>0|Im~=UEil6IUUbso4iSZsMgdnq7 zpF&Q8osM>9KSeKIw_CKE@n_PvB_2Iirxzk{4ntrmg=G8CS zSGT^y70n%)jHV_vsk7gV9X9AthiALGU2@%ar8!`0N_~J?q4!Hn*yO+tE!d|f2U_qo zdjIS|B~vUb&^GXVq)E&QM7DV&(tWs>U;VjN5A|AE>4i#6LQ6WNzOm`C;#pFccg6oy2A;TWtFkNR5QLWUy(A4QoB(*l*laO6)>X8DvrWb2X~&#F>%@%HsE4><8_862eO#i9l|8q(TJ z&(=SE^P@G~972K1jX9)NlQ5v+{O%PGzV>u>w=)4dgTI`Lt3^H@%xg&CIqoMUI}ZP1R;oEfEJ>%|s->gdJY(N_NQ++qD-`c3EJW7{$drw6KqBJJ_%ftu05 z+Sch=CuVl7viPcEuG3jwoafBR=?uHEnK?hu!(`3~T5Z*MQhpp|X$3A1rdU_4z7UK{8W-J`K>snoF{ zF-aS4*8Z~5X4`c%{RI{=SbW-L*`gO;@3ZQHMKNn8)R9oQa!04dw|n+WZnxZ+*hG%{bN8UT?<;t9*cf-N+G-1a z#2lZ;Y2i5xDq%47-b20_4WbkW-d6VxIl4*&W=A}>xK8=$v7&it~81v z2J2pmDz$SL%tfv?&AFgs*0h6d`>F@mA81_u@KIWz#D((a!1b*Di}cXja~?CLZ@?M$ zTHhjiQ<9yWTf%D`-I#OFdsNpN-xKVQnWZ=2Ja_3vnP+ccf4Rzh09AcdC%d7w-O+wT zoruMsxnF|tJ3Gw{NF42FC$d-`NIO{r26lRsm;X~6HT(1+$Rh9J3c=;^wZz&|IJ3S za?Uh0lNS;!?qaGOCdyqjYE*x81mYKSU`e&OZi0bou*eJd8>)DN_UH!vN-gLL-sYZMAtcqI0=)X2x zVj>o^%2xNZd*Ekt)M{i2Dc6UvfF+KOU;V}%9FjU0vFnyMC)p#b^ zq%WmIr+b^*m(t9`X2((v0K5B`;>)04>SGclpXzHSEhGC9v*Z=rSj((mh80)qZ^`9a zU;7r=d8XWoK$RLCYxH13C(RRk=1#f&z1QBR+4v^Sxrq2yn=DM*+|%FoYuOVKU#`7t zLj`vN+%@_LITa}SVf2iSXXj1PJ)TbbIF#0&zJ>j#lPSHN)_R%x%UN=74KTx&(}%KV zDQu`q=KbZYEVo}(iW4Vp*P>d-U1|5AM<35>_fh07-FA4}#9ud?YeuZVnd}21!txYuQE+;#bwqxmzl#kfp96SA*0P;TT8_B zPp^2Y*yC~3qT>*w+h9JumHBn=LRtpC9&GX*&O@GT^XGfO;`Wlv7XR*0dpx*gWyL*T z-TFlYHEQP~l#dNvy@@KmA8MYxD^OWpC}#XC@myK6@lNK6H58p4$0cTnx%H>gV2?vGu-`%zd5#MQ!cWRbfq_nptrNhjb>sjybTBQ8Y0%SK+hx zVfOGk_}(4M+m=}Mm{*#f+-)*fF2TwW0Vd4k(zcjxYAI-3h!{!>wNj|s$7PZ_~hjJNJAyJpw)rB8Oc z;4QCVuc6{JyjPJ-)L!vWDywb4FsCDZP$<;k9N3 z4DC-9`WKtI1?*F|Ut_OG$H#tl?B|7ZdS4jV{boA`MZEkxrrMLr6~E(?Q7x+Hd!ryI z4qmf*ZJ@F23chebSb@*49da}_vyjroe| z{bkJddkHBbD9T^Q47d%^%uGZ?|MmR0GwU&E-7VF|bDQ3?8#P(~T@xD7$(6-0kQ@!F zZeI7;AHJzo$sH8eWPUYaj;rom4BYe7UHy8cZhK9q53M7dztGgaoj$BH?Ge>?V8@MB ze&nTJ*8XVu+Pa=yJkPIF&6L}@Wjlg}ZrLhzZhiUm=im6ivmpFVc{%g$e&Le$i!beP zA*Y<#P36(GN83)EC{uMp`dh#D@eJr?Gjmd%xG>qv?`GX3t_!rX(NaYhQ#`qB9iH^U z<#zKNk>Q1rw&hJ@?2T&WWwUDkR=EG83)Po3<%qb}Rma-RxAZ6Nze;vicneI&poZj> zXHdlk&0PG&>gqba%RNk=yVk4Jz{d&LXp7uXP^wMMT{nN{+0d{X$x&Whhq~uQv)fCz z`?lMr*@i&0roBq>wig+-+Pb@`o}P*&^M=Udc4w z$h}~SS#lR{v?1m-^Y2CeF5~=uR_jhK41ZY6Xhu_M*I#I|%z--tG0Ji`Lc4=s*bsM` zj_Od+K2|hSMcJ-~>hwAp+6A>g|8WquC!r46%R)`!=zFdy6`05#dpcI>o85PaJCU4p zwGp|!{nOqYMi_W?{rlC%b}jlO>tAR1(uF^+8v0o2sa~CwEM`XCL+t-W^bq@Apt$`D z1(7*?4;!W33IA=p?scweOiL~cHX`fysl8!O(uWn@M%~@qy9P3(PyTXaayKG(XVnb- z%?|dLW537r*~GNCOC*ZFVKPtJ1asRaPH6VFz4bdhebp^&!;>FoXIP8sFI`)q z`I?$KdGesGN%I!Ipn2{-np$VhJwU^iO}YCKHB9XNoLAxukDy!n6U_aH01qb*-_Mfl zX7ub)=cJ1~IrX0XwKH*4ry_cuX^TX0+MlCF!p*7&xZAQ#Mql&>nA4wbfSYfn67D^E zZ{AQD)87KzhiUeJOeyZFM)Tw=smrOa{Wc5sfsDZDXT+c8>XG8yD8f zrsji8SKVvvDeBIa@Ah4P&*^w?QW)kgak@f(c@>T+`@jvakA4(;O)v9xD>=H^KOIqLN99f%P6YfndQ7(I{B)o) zPa9i?HWQvh{(tdn44%8)vK`0WWVUQ)Xv@tZi2_sMVW#5w>rLZ_S@UmQZ!de#G`{Wq z_JbmOxxR9r+#X3V*F20<-Dg}?bfOz%wm!^F5k=2ZG`h-s>w=p~#@^BAu7@|dMVmL_ znl{=reMI+I3(R$oV5lz_Y6r+q?DzC9`2z-G=-rfbH`^a^rxxMH;TcSIgq>7u>fWY}kFTOGJ$N9-s+m5IQrBiP{!xax%iM-m{awtp z(a-*%X8G5~#N~Q@raM=U=flUyE*r{|m zdsc3%#=^~dnO8Equ;Y2eaf#ba?Z+UunD!F!X3%4SmaRuEvvm}Ea9pLC^;c>{-t*x} z32>MrbN z-(q)_jSqL;|8b*zx(C)TM5t(~NqQW+b>`!poHhvCt!Bhd2)BG?^X$$*@is3|S}vMU zId6IPV>NE4G`~<#>3@+^iJagIJGOmr^=Ge=X z#G|d_SK2765|$FvH|7TJ3KN>NCT9664LQ;Hq+{PHxiJf#*9n*HnJ2QXt~Tc=8hGn^ z^X>CgINLOTf{oX`2v#u@UkJn#T>GD(jTL713+|-&pJeZR$RzEd*fT55usxieSar)J z-&kqBd6L!V{`RdUzru@sk$vglN;C3Ba?h9`x>ZZl*z@wv?`K?}UR%G|p<(4x(ORd(sm+VxX>Si7FOcInv+YvXQmv|u0E zmv`&s;~%I`j-ISiA!W-x&656W3wQf#89kxE?zDpkyxKRl{G%^Zg%>;1%`H#km%zlRP4mVq$2|1^VIrL1Ra`2nm?F~tHbA~MMF$DL4?&F9S)`9Nt zXF8d=&r)`nG0)P#9P|0JxC|FndoHlnU)=0?&Z_cQv|fIz$PoK&Juvh5dr__FpRV}~ zah+jaDw*cb2db9pey5%7i$)16d_6G}{g)xp^8d#nQPIS`MBgvF%RaT=``YVYRqH=e z_mgd!xR>l+CKUtz#@rlUohAR3_Np}`RyeF zbMIXy@?`?`g+?S6IQzP3KoH^v`WfPaAF#Y5N{s<3799uXy~07c%NJUl{Onx^hpt?ad+C z+2Lr_H-3#S2F<9~Xu$TgVlO5s3U7@oOnP`wT*Bw&`3r>4ycUS68yfguPs@Ym42IFq z(Y}6pf5(ic;xgU}%Xac_Yp&fNXnfwQp2KZN%@g~$D{=eP=#o(L3ttWR^x6BG z>F|2ss=Ph5tpD!4XLkO3FMapzq>P+t>1mUF37_mcF?w*-kr&G++tR z-v}gCt49PWisvV^e$79t>~E)DvwrwwmU-okK!KV3W}xNc|IeOXVEW0=U7VSomzblQ OT9H|@{r&@{8?ynekk^L* diff --git a/package.json b/package.json index 55c2f40..6691370 100644 --- a/package.json +++ b/package.json @@ -40,9 +40,11 @@ "pathe": "^1.1.2", "pretty-bytes": "^6.1.1", "rc9": "^2.1.1", + "signal-exit": "^4.1.0", "ufo": "^1.4.0", "unstorage": "^1.10.1", - "update-notifier": "^7.0.0" + "update-notifier": "^7.0.0", + "ws": "^8.16.0" }, "devDependencies": { "@nuxt/eslint-config": "^0.2.0", diff --git a/src/commands/logs.mjs b/src/commands/logs.mjs new file mode 100644 index 0000000..9977d94 --- /dev/null +++ b/src/commands/logs.mjs @@ -0,0 +1,91 @@ +import { consola } from 'consola' +import { colors } from 'consola/utils' +import { onExit } from 'signal-exit' +import { setTimeout } from 'timers/promises' +import { defineCommand, runCommand } from 'citty' +import { isCancel, confirm } from '@clack/prompts' +import { fetchUser, projectPath, fetchProject } from '../utils/index.mjs' +import login from './login.mjs' +import { connectLogs, createLogs, deleteLogs, printFormattedLog } from '../utils/logs.mjs' +import link from './link.mjs' + +export default defineCommand({ + meta: { + name: 'logs', + description: 'Display the logs of a deployment.', + }, + args: { + production: { + type: 'boolean', + description: 'Display the logs of the production deployment.', + default: false + }, + preview: { + type: 'boolean', + description: 'Display the logs of the preview deployment.', + default: true + } + }, + async setup({ args }) { + let user = await fetchUser() + if (!user) { + consola.info('Please login to deploy your project.') + await runCommand(login, {}) + user = await fetchUser() + } + + const env = args.production ? 'production' : 'preview' + let project = await fetchProject() + if (!project) { + consola.warn(`${colors.blue(projectPath())} is not linked to any NuxtHub project.`) + + const shouldLink = await confirm({ + message: 'Do you want to link it to a project?', + initialValue: false + }) + if (!shouldLink || isCancel(shouldLink)) { + return + } + await runCommand(link, {}) + project = await fetchProject() + if (!project) { + return console.log('project is null') + } + } + + consola.start(`Connecting to ${env} deployment...`) + + const logs = await createLogs(project.slug, project.teamSlug, env) + + const socket = connectLogs(logs.url) + + const onCloseSocket = async () => { + socket.terminate() + await deleteLogs(project.slug, project.teamSlug, env, logs.id) + } + + onExit(onCloseSocket) + socket.on('close', onCloseSocket) + + socket.on('message', (data) => { + printFormattedLog(data) + }) + + while (socket.readyState !== socket.OPEN) { + switch (socket.readyState) { + case socket.CONNECTING: + await setTimeout(100) + break + case socket.CLOSING: + await setTimeout(100) + break + case socket.CLOSED: + throw new Error( + 'Connection to deployment closed unexpectedly.' + ) + } + } + + consola.success(`Connected to ${env} deployment waiting for logs...`) + }, +}) diff --git a/src/index.mjs b/src/index.mjs index 64c9e5c..81ff150 100755 --- a/src/index.mjs +++ b/src/index.mjs @@ -11,6 +11,7 @@ import link from './commands/link.mjs' import unlink from './commands/unlink.mjs' import login from './commands/login.mjs' import logout from './commands/logout.mjs' +import logs from './commands/logs.mjs' import whoami from './commands/whoami.mjs' import deploy from './commands/deploy.mjs' import open from './commands/open.mjs' @@ -40,6 +41,7 @@ const main = defineCommand({ manage, login, logout, + logs, whoami }, }) diff --git a/src/utils/logs.mjs b/src/utils/logs.mjs new file mode 100644 index 0000000..f8d7789 --- /dev/null +++ b/src/utils/logs.mjs @@ -0,0 +1,144 @@ +import { consola } from 'consola' +import WebSocket from 'ws' +import { $api } from './data.mjs' + +const CF_LOG_OUTCOMES = { + ok: 'OK', + canceled: 'Canceled', + exceededCpu: 'Exceeded CPU Limit', + exceededMemory: 'Exceeded Memory Limit', + exception: 'Exception Thrown', + unknown: 'Unknown' +} + + +export async function createLogs(projectSlug, teamSlug, env) { + return await $api(`/teams/${teamSlug}/projects/${projectSlug}/${env}/logs`) +} + +export async function deleteLogs(projectSlug, teamSlug, env, id) { + return await $api(`/teams/${teamSlug}/projects/${projectSlug}/${env}/logs/${id}`, { + method: 'DELETE' + }) +} + +export function connectLogs(url, debug = false) { + const tail = new WebSocket(url, 'trace-v1', { + headers: { + 'Sec-WebSocket-Protocol': 'trace-v1', // needs to be `trace-v1` to be accepted + 'User-Agent': `nuxt-hub/${11}`, + }, + }) + + // send filters when we open up + tail.on('open', () => { + tail.send( + JSON.stringify({ debug: debug }), + { binary: false, compress: false, mask: false, fin: true }, + (err) => { + if (err) { + throw err + } + } + ) + }) + + return tail +} + +export function printFormattedLog(log) { + log = JSON.parse(log.toString()) + const outcome = CF_LOG_OUTCOMES[log.outcome] || CF_LOG_OUTCOMES.unknown + + // Request + if ('request' in log.event) { + const { request: { method, url }, response: { status } } = log.event + const datetime = new Date(log.eventTimestamp).toLocaleString() + + consola.log( + url + ? `${method.toUpperCase()} ${url} - ${outcome} ${status} @${datetime}` + : `[missing request] - ${outcome} @${datetime}` + ) + return + } + + // Cron + if ('cron' in log.event) { + const cronPattern = log.event.cron + const datetime = new Date(log.event.scheduledTime).toLocaleString() + const outcome = log.outcome + + consola.log(`"${cronPattern}" @${datetime} - ${outcome}`) + return + } + + // Email + if ('mailFrom' in log.event) { + const datetime = new Date(log.eventTimestamp).toLocaleString() + const mailFrom = log.event.mailFrom + const rcptTo = log.event.rcptTo + const rawSize = log.event.rawSize + + consola.log(`Email from:${mailFrom} to:${rcptTo} size:${rawSize} @${datetime} - ${outcome}`) + return + } + + // Alarm + if ('scheduledTime' in log.event && !('cron' in log.event)) { + const datetime = new Date(log.event.scheduledTime).toLocaleString() + consola.log(`Alarm @${datetime} - ${outcome}`) + return + } + + // Tail Event + if ('consumedEvents' in log.event) { + const datetime = new Date(log.eventTimestamp).toLocaleString() + const tailedScripts = new Set( + log.event.consumedEvents + .map((consumedEvent) => consumedEvent.scriptName) + .filter((scriptName) => !!scriptName) + ) + + consola.log(`Tailing ${Array.from(tailedScripts).join(',')} - ${outcome} @${datetime}`) + return + } + + // Tail Info + if ('message' in log.event && 'type' in log.event) { + if (log.event.type === 'overload') { + consola.log(log.event.message) + } else if (log.event.type === 'overload-stop') { + consola.log(log.event.message) + } + return + } + + // Queue + if ('queue' in log.event) { + const datetime = new Date(log.eventTimestamp).toLocaleString() + const queueName = log.event.queue + const batchSize = log.event.batchSize + const batchSizeMsg = `${batchSize} message${batchSize !== 1 ? 's' : ''}` + + consola.log(`Queue ${queueName} (${batchSizeMsg}) - ${outcome} @${datetime}`) + return + } + + // Unknown event type + const datetime = new Date(log.eventTimestamp).toLocaleString() + consola.log(`Unknown Event - ${outcome} @${datetime}`) + + // Print console logs and exceptions + if (log.logs.length > 0) { + log.logs.forEach(({ level, message }) => { + consola.log(` (${level})`, ...message) + }) + } + + if (log.exceptions.length > 0) { + log.exceptions.forEach(({ name, message }) => { + consola.error(` ${name}:`, message) + }) + } +} \ No newline at end of file From 87e606883dca9160b30bed7801151398c7aa8431 Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Mon, 25 Mar 2024 17:47:52 +0100 Subject: [PATCH 2/7] fix: don't return after log --- src/utils/logs.mjs | 53 +++++++++++++++------------------------------- 1 file changed, 17 insertions(+), 36 deletions(-) diff --git a/src/utils/logs.mjs b/src/utils/logs.mjs index f8d7789..cc60c9b 100644 --- a/src/utils/logs.mjs +++ b/src/utils/logs.mjs @@ -50,8 +50,8 @@ export function printFormattedLog(log) { log = JSON.parse(log.toString()) const outcome = CF_LOG_OUTCOMES[log.outcome] || CF_LOG_OUTCOMES.unknown - // Request if ('request' in log.event) { + // Request const { request: { method, url }, response: { status } } = log.event const datetime = new Date(log.eventTimestamp).toLocaleString() @@ -60,39 +60,27 @@ export function printFormattedLog(log) { ? `${method.toUpperCase()} ${url} - ${outcome} ${status} @${datetime}` : `[missing request] - ${outcome} @${datetime}` ) - return - } - - // Cron - if ('cron' in log.event) { + } else if ('cron' in log.event) { + // Cron const cronPattern = log.event.cron const datetime = new Date(log.event.scheduledTime).toLocaleString() const outcome = log.outcome consola.log(`"${cronPattern}" @${datetime} - ${outcome}`) - return - } - - // Email - if ('mailFrom' in log.event) { + } else if ('mailFrom' in log.event) { + // Email const datetime = new Date(log.eventTimestamp).toLocaleString() const mailFrom = log.event.mailFrom const rcptTo = log.event.rcptTo const rawSize = log.event.rawSize consola.log(`Email from:${mailFrom} to:${rcptTo} size:${rawSize} @${datetime} - ${outcome}`) - return - } - - // Alarm - if ('scheduledTime' in log.event && !('cron' in log.event)) { + } else if ('scheduledTime' in log.event && !('cron' in log.event)) { + // Alarm const datetime = new Date(log.event.scheduledTime).toLocaleString() consola.log(`Alarm @${datetime} - ${outcome}`) - return - } - - // Tail Event - if ('consumedEvents' in log.event) { + } else if ('consumedEvents' in log.event) { + // Tail Event const datetime = new Date(log.eventTimestamp).toLocaleString() const tailedScripts = new Set( log.event.consumedEvents @@ -101,34 +89,27 @@ export function printFormattedLog(log) { ) consola.log(`Tailing ${Array.from(tailedScripts).join(',')} - ${outcome} @${datetime}`) - return - } - - // Tail Info - if ('message' in log.event && 'type' in log.event) { + } else if ('message' in log.event && 'type' in log.event) { + // Tail Info if (log.event.type === 'overload') { consola.log(log.event.message) } else if (log.event.type === 'overload-stop') { consola.log(log.event.message) } - return - } - - // Queue - if ('queue' in log.event) { + } else if ('queue' in log.event) { + // Queue const datetime = new Date(log.eventTimestamp).toLocaleString() const queueName = log.event.queue const batchSize = log.event.batchSize const batchSizeMsg = `${batchSize} message${batchSize !== 1 ? 's' : ''}` consola.log(`Queue ${queueName} (${batchSizeMsg}) - ${outcome} @${datetime}`) - return + } else { + // Unknown event type + const datetime = new Date(log.eventTimestamp).toLocaleString() + consola.log(`Unknown Event - ${outcome} @${datetime}`) } - // Unknown event type - const datetime = new Date(log.eventTimestamp).toLocaleString() - consola.log(`Unknown Event - ${outcome} @${datetime}`) - // Print console logs and exceptions if (log.logs.length > 0) { log.logs.forEach(({ level, message }) => { From 075a1eea0eebe56ee8529aae00b8dc992ae92f04 Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Mon, 25 Mar 2024 17:54:50 +0100 Subject: [PATCH 3/7] fix: remove tail on socket error --- src/commands/logs.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/commands/logs.mjs b/src/commands/logs.mjs index 9977d94..257a5bf 100644 --- a/src/commands/logs.mjs +++ b/src/commands/logs.mjs @@ -80,9 +80,9 @@ export default defineCommand({ await setTimeout(100) break case socket.CLOSED: - throw new Error( - 'Connection to deployment closed unexpectedly.' - ) + consola.error('Connection to deployment closed unexpectedly.') + await onCloseSocket() + process.exit(1) } } From 394de4da5ed10c0a1f80549870272ee2b3a70c6e Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Mon, 25 Mar 2024 18:17:19 +0100 Subject: [PATCH 4/7] fix: error logs --- src/utils/logs.mjs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/utils/logs.mjs b/src/utils/logs.mjs index cc60c9b..8eeed99 100644 --- a/src/utils/logs.mjs +++ b/src/utils/logs.mjs @@ -1,4 +1,5 @@ import { consola } from 'consola' +import { colors } from 'consola/utils' import WebSocket from 'ws' import { $api } from './data.mjs' @@ -113,13 +114,17 @@ export function printFormattedLog(log) { // Print console logs and exceptions if (log.logs.length > 0) { log.logs.forEach(({ level, message }) => { - consola.log(` (${level})`, ...message) + if (level === 'error') { + consola.error(...message) + } else { + consola.log(` (${level})`, ...message) + } }) } if (log.exceptions.length > 0) { log.exceptions.forEach(({ name, message }) => { - consola.error(` ${name}:`, message) + consola.error(colors.red(` ${name}:`, message)) }) } } \ No newline at end of file From 9ddeaf56ac8578acf9af805f9b751e1b7ac64a4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Chopin?= Date: Mon, 25 Mar 2024 19:25:56 +0100 Subject: [PATCH 5/7] chore: improvements --- bun.lockb | Bin 228535 -> 228151 bytes src/commands/logs.mjs | 31 +++++++++++++++++++------------ src/commands/manage.mjs | 2 +- src/commands/open.mjs | 16 +++++----------- src/utils/git.mjs | 15 +++++++++++++++ src/utils/index.mjs | 1 + src/utils/logs.mjs | 10 ++++++---- 7 files changed, 47 insertions(+), 28 deletions(-) diff --git a/bun.lockb b/bun.lockb index 21a1561d61c36d590782bdc649373f2c406f9e54..483b1d8e24dfbdc9ecca28e5c57fc72747f61f68 100755 GIT binary patch delta 3377 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 diff --git a/src/commands/logs.mjs b/src/commands/logs.mjs index 257a5bf..a034a66 100644 --- a/src/commands/logs.mjs +++ b/src/commands/logs.mjs @@ -1,12 +1,12 @@ import { consola } from 'consola' import { colors } from 'consola/utils' +import ora from 'ora' import { onExit } from 'signal-exit' import { setTimeout } from 'timers/promises' import { defineCommand, runCommand } from 'citty' import { isCancel, confirm } from '@clack/prompts' -import { fetchUser, projectPath, fetchProject } from '../utils/index.mjs' +import { fetchUser, projectPath, fetchProject, getProjectEnv, connectLogs, createLogs, deleteLogs, printFormattedLog } from '../utils/index.mjs' import login from './login.mjs' -import { connectLogs, createLogs, deleteLogs, printFormattedLog } from '../utils/logs.mjs' import link from './link.mjs' export default defineCommand({ @@ -22,8 +22,8 @@ export default defineCommand({ }, preview: { type: 'boolean', - description: 'Display the logs of the preview deployment.', - default: true + description: 'Display the logs of the latest preview deployment.', + default: false } }, async setup({ args }) { @@ -34,7 +34,6 @@ export default defineCommand({ user = await fetchUser() } - const env = args.production ? 'production' : 'preview' let project = await fetchProject() if (!project) { consola.warn(`${colors.blue(projectPath())} is not linked to any NuxtHub project.`) @@ -49,14 +48,22 @@ export default defineCommand({ await runCommand(link, {}) project = await fetchProject() if (!project) { - return console.log('project is null') + return consola.error('Could not fetch the project, please try again.') } } + const env = getProjectEnv(project, args) + const envColored = env === 'production' ? colors.green(env) : colors.yellow(env) + const url = (env === 'production' ? project.url : project.previewUrl) + if (!url) { + consola.info(`No deployment found for ${envColored} environment.`) + return consola.info(`Please run \`nuxthub deploy --${env}\` to deploy your project.`) + } + consola.success(`Linked to ${colors.blue(project.slug)} project available at \`${url}\``) - consola.start(`Connecting to ${env} deployment...`) + const spinner = ora(`Connecting to ${envColored} deployment...`).start() const logs = await createLogs(project.slug, project.teamSlug, env) - + const socket = connectLogs(logs.url) const onCloseSocket = async () => { @@ -66,11 +73,11 @@ export default defineCommand({ onExit(onCloseSocket) socket.on('close', onCloseSocket) - + socket.on('message', (data) => { printFormattedLog(data) }) - + while (socket.readyState !== socket.OPEN) { switch (socket.readyState) { case socket.CONNECTING: @@ -85,7 +92,7 @@ export default defineCommand({ process.exit(1) } } - - consola.success(`Connected to ${env} deployment waiting for logs...`) + + spinner.succeed(`Connected to ${envColored} deployment waiting for logs...`) }, }) diff --git a/src/commands/manage.mjs b/src/commands/manage.mjs index b245982..3ff6698 100644 --- a/src/commands/manage.mjs +++ b/src/commands/manage.mjs @@ -34,7 +34,7 @@ export default defineCommand({ await runCommand(link, {}) project = await fetchProject() if (!project) { - return console.log('project is null') + return console.error('Could not fetch the project, please try again.') } } diff --git a/src/commands/open.mjs b/src/commands/open.mjs index 66f78bc..a28ab22 100644 --- a/src/commands/open.mjs +++ b/src/commands/open.mjs @@ -2,7 +2,7 @@ import { consola } from 'consola' import { colors } from 'consola/utils' import { isCancel, confirm } from '@clack/prompts' import { defineCommand, runCommand } from 'citty' -import { fetchUser, projectPath, fetchProject, gitInfo } from '../utils/index.mjs' +import { fetchUser, projectPath, fetchProject, getProjectEnv } from '../utils/index.mjs' import open from 'open' import login from './login.mjs' import link from './link.mjs' @@ -45,20 +45,14 @@ export default defineCommand({ await runCommand(link, {}) project = await fetchProject() if (!project) { - return console.log('project is null') + return console.error('Could not fetch the project, please try again.') } } // Get the environment based on branch - let env = 'production' - if (args.preview) { - env = 'preview' - } else if (!args.production && !args.preview) { - const git = gitInfo() - // Guess the env based on the branch - env = (git.branch === project.productionBranch) ? 'production' : 'preview' - } + const env = getProjectEnv(project, args) const envColored = env === 'production' ? colors.green(env) : colors.yellow(env) const url = (env === 'production' ? project.url : project.previewUrl) + consola.info(`Opening ${envColored} URL of ${colors.blue(project.slug)} in the browser...`) if (!url) { consola.info(`Project ${colors.blue(project.slug)} does not have a ${envColored} URL, please run \`nuxthub deploy --${env}\`.`) @@ -67,6 +61,6 @@ export default defineCommand({ open(url) - consola.success(`Project \`${url}\` opened in the browser.`) + consola.success(`\`${url}\` opened in the browser.`) }, }) diff --git a/src/utils/git.mjs b/src/utils/git.mjs index d318e92..bb65147 100644 --- a/src/utils/git.mjs +++ b/src/utils/git.mjs @@ -29,3 +29,18 @@ export function gitInfo() { } return git } + +export function getProjectEnv(project, args) { + if (args.production) { + return 'production' + } + if (args.preview) { + return 'preview' + } + // Guess from git branch + const git = gitInfo() + if (!git.branch || git.branch === project?.productionBranch) { + return 'production' + } + return 'preview' +} diff --git a/src/utils/index.mjs b/src/utils/index.mjs index f69192e..03b2556 100644 --- a/src/utils/index.mjs +++ b/src/utils/index.mjs @@ -3,3 +3,4 @@ export * from './data.mjs' export * from './deploy.mjs' export * from './git.mjs' export * from './poll.mjs' +export * from './logs.mjs' diff --git a/src/utils/logs.mjs b/src/utils/logs.mjs index 8eeed99..d08cd39 100644 --- a/src/utils/logs.mjs +++ b/src/utils/logs.mjs @@ -54,12 +54,14 @@ export function printFormattedLog(log) { if ('request' in log.event) { // Request const { request: { method, url }, response: { status } } = log.event - const datetime = new Date(log.eventTimestamp).toLocaleString() + const statusColored = status >= 500 ? colors.red(status) : status >= 400 ? colors.yellow(status) : colors.green(status) + // const datetime = new Date(log.eventTimestamp).toLocaleString() + const path = new URL(url).pathname consola.log( url - ? `${method.toUpperCase()} ${url} - ${outcome} ${status} @${datetime}` - : `[missing request] - ${outcome} @${datetime}` + ? `${method.toUpperCase().padStart(6, ' ')} ${statusColored} ${path}` + : `[missing request] - ${outcome}` ) } else if ('cron' in log.event) { // Cron @@ -127,4 +129,4 @@ export function printFormattedLog(log) { consola.error(colors.red(` ${name}:`, message)) }) } -} \ No newline at end of file +} From e805a8565e85b7ddced0849f7be95e40821c0abf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Chopin?= Date: Mon, 25 Mar 2024 19:34:11 +0100 Subject: [PATCH 6/7] chore: update --- src/utils/logs.mjs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/utils/logs.mjs b/src/utils/logs.mjs index d08cd39..fdbebe3 100644 --- a/src/utils/logs.mjs +++ b/src/utils/logs.mjs @@ -116,17 +116,13 @@ export function printFormattedLog(log) { // Print console logs and exceptions if (log.logs.length > 0) { log.logs.forEach(({ level, message }) => { - if (level === 'error') { - consola.error(...message) - } else { - consola.log(` (${level})`, ...message) - } + consola[level](...message) }) } if (log.exceptions.length > 0) { log.exceptions.forEach(({ name, message }) => { - consola.error(colors.red(` ${name}:`, message)) + consola.error(colors.red(`${name}:`, message)) }) } } From 08d5600d6baf639d208671d0fd7a4bb3ec9184e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Chopin?= Date: Mon, 25 Mar 2024 19:38:59 +0100 Subject: [PATCH 7/7] chore: update readme --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2d9c0e5..c00531a 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ npx nuxthub ## Usage ```bash -USAGE nuxthub init|deploy|link|unlink|open|manage|login|logout|whoami +USAGE nuxthub init|deploy|link|unlink|open|manage|login|logout|logs|whoami COMMANDS @@ -31,6 +31,7 @@ COMMANDS manage Open in browser the NuxtHub URL for a linked project. login Authenticate with NuxtHub. logout Logout the current authenticated user. + logs Display the logs of a deployment. whoami Shows the username of the currently logged in user. Use nuxthub --help for more information about a command.