From 1c7c81f5439898be39a59aa531023a0264a21828 Mon Sep 17 00:00:00 2001 From: John Vajda Date: Sat, 7 Feb 2026 08:21:41 -0700 Subject: [PATCH 1/7] adds generate command + testing fixes --- README.md | 60 ++- TEST_PLAN.md | 116 ------ images/downfolio.png | Bin 0 -> 55052 bytes src/cli.ts | 18 + src/commands/convert.ts | 206 ++++++++++ src/commands/job.ts | 28 +- src/commands/template.ts | 28 +- src/lib/ai.ts | 31 +- src/lib/files.ts | 14 +- src/lib/pandoc.ts | 46 ++- tests/TEST_PLAN.md | 121 ++++++ tests/unit/commands/convert.test.ts | 566 +++++++++++++++++++++++++++ tests/unit/commands/job.test.ts | 45 ++- tests/unit/commands/template.test.ts | 45 ++- tests/unit/lib/files.test.ts | 59 +-- 15 files changed, 1202 insertions(+), 181 deletions(-) delete mode 100644 TEST_PLAN.md create mode 100644 images/downfolio.png create mode 100644 src/commands/convert.ts create mode 100644 tests/TEST_PLAN.md create mode 100644 tests/unit/commands/convert.test.ts diff --git a/README.md b/README.md index c07aac0..5df85ad 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,55 @@ # Downfolio -AI-powered CLI tool for generating customized resumes and cover letters from markdown templates. +Markdown + Porfolio = Downfolio + +AI-powered CLI tool for generating customized resumes and cover letters from markdown templates based on your career experience. + + +![image](./images/downfolio.png) + +## Recommended Use + +1. Create base templates for cover letter and resumes that you write yourself based on your career experience. (Don't Use AI) +2. For every job you want to apply for use Downfolio to generate custom resumes and cover letters from your base templates. +3. Review the output carefully and ensure it reflects your work experience. ## Installation +### Prerequisites + +Downfolio requires the following dependencies for document conversion: + +**Required for DOCX and PDF conversion:** +- **Pandoc** - Document converter + ```bash + # macOS + brew install pandoc + + # Other platforms: https://pandoc.org/installing.html + ``` + +**Required for PDF conversion only:** +- **PDF Engine** - One of the following: + - **BasicTeX** (recommended, ~100MB) + ```bash + # macOS + brew install --cask basictex + # Then restart your terminal or run: + eval "$(/usr/libexec/path_helper)" + ``` + - **MacTeX** (full distribution, ~4GB) + ```bash + brew install --cask mactex-no-gui + ``` + - **wkhtmltopdf** (alternative) + ```bash + brew install wkhtmltopdf + ``` + +**Note:** Markdown format requires no dependencies. DOCX requires only Pandoc. PDF requires both Pandoc and a PDF engine. + +### Install Downfolio + ```bash pnpm install pnpm run build @@ -126,16 +172,6 @@ pnpm run watch ## Testing -TBD - -## CLI Commands +See [tests](./tests/) -- `downfolio init` - Initialize project -- `downfolio config` - Manage configuration -- `downfolio template` - Manage templates -- `downfolio job` - Manage job descriptions -- `downfolio generate` - Generate documents -- `downfolio validate` - Validate markdown files -- `downfolio preview` - Preview markdown files -All commands support interactive mode with Clack prompts when flags are omitted. diff --git a/TEST_PLAN.md b/TEST_PLAN.md deleted file mode 100644 index 3c3f625..0000000 --- a/TEST_PLAN.md +++ /dev/null @@ -1,116 +0,0 @@ -# Downfolio Test Plan - -## Overview - -This test plan covers manual testing for the Downfolio CLI tool. For unit testing see `/tests`. - -### Manual End-to-End Tests - -#### 3.1 Complete Workflow Test -**Setup:** -1. [x] Initialize project (`downfolio init`) -2. [ ] Add resume template (`downfolio template add`) -3. [ ] Add cover letter template (`downfolio template add`) -4. [ ] Add job description (`downfolio job add`) - -**Generation:** -5. [ ] Generate documents (`downfolio generate --job `) -6. [ ] Verify output files created -7. [ ] Verify markdown output exists -8. [ ] Verify docx output exists (if selected) -9. [ ] Verify pdf output exists (if selected) - -**Cleanup:** -10. [ ] Remove job (`downfolio job remove`) -11. [ ] Remove templates (`downfolio template remove`) -12. [ ] Verify cleanup successful - -#### 3.2 Multiple Jobs Workflow -1. [ ] Initialize project -2. [ ] Add multiple jobs -3. [ ] Generate documents for each job -4. [ ] Verify separate output folders created -5. [ ] Verify no conflicts between outputs - -#### 3.3 Global vs Project Config -1. [x] Initialize global config -2. [x] Initialize project config -3. [ ] Verify project config takes precedence -4. [ ] Verify templates/jobs stored in correct location - -### 4. Manual Testing Checklist - -#### 4.1 Installation & Setup -- [x] `npm install` completes successfully -- [x] `npm run build` compiles TypeScript -- [x] `npm link` makes command globally available -- [x] `downfolio --version` shows correct version -- [x] `downfolio --help` shows help text - -#### 4.2 Interactive Prompts (Clack) -- [ ] All prompts display correctly -- [ ] Keyboard navigation works (arrow keys, Enter) -- [ ] Cancellation (Ctrl+C) works gracefully -- [ ] Spinners animate correctly -- [ ] Success/error messages display correctly -- [ ] Colors and formatting render properly - -#### 4.3 Error Handling -- [ ] Invalid commands show helpful error messages -- [ ] Missing required flags show helpful prompts -- [ ] File not found errors are clear -- [ ] Permission errors are handled gracefully -- [ ] Network errors (for future AI calls) are handled - -#### 4.4 File Operations -- [ ] Files are created in correct locations -- [ ] File permissions are correct -- [ ] Directory structure is created correctly -- [ ] Config files are readable/writable -- [ ] Storage files persist correctly - -### 5. Edge Cases - -- [ ] Very long file paths -- [ ] Special characters in file names -- [ ] Empty files -- [ ] Very large files (>10MB) -- [ ] Concurrent operations (multiple CLI instances) -- [ ] Missing dependencies (Pandoc, etc.) -- [ ] Corrupted config files -- [ ] Corrupted storage files -- [ ] Disk full scenarios -- [ ] Read-only file system - -### 6. Security Tests - -- [ ] API keys are masked in output -- [ ] Config files have appropriate permissions -- [ ] No sensitive data in logs -- [ ] File paths are validated (no directory traversal) -- [ ] Input sanitization for user-provided data - -## Test Execution - -### Automated Tests -```bash -# Run all tests -npm test - -# Run unit tests only -npm run test:unit - -# Run integration tests only -npm run test:integration - -# Run with coverage -npm run test:coverage -``` - -### Manual Tests -1. Follow the manual testing checklist above -2. Test each command in both interactive and non-interactive modes -3. Test error scenarios -4. Test on different platforms if available - - diff --git a/images/downfolio.png b/images/downfolio.png new file mode 100644 index 0000000000000000000000000000000000000000..91b52aa9e79a8c341c16ca6f94e0a430fdcd7102 GIT binary patch literal 55052 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

O;X0zO% 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 z z0cf0y!v#OUk>5H#Ad$e@C&NjQw@0=)XSY2rabry74@soy$^St<7$H)>@iw&43{$D_ z0}|g&ZqRz#x0X@L6XX7qiTt|6t!E)A!5A2rqd-y`z=y1ao97%oHrQYL`Jtkz_K#{6 zzvJbVyw9CbKk;7n#WhctXerDYS}2aHcLQrx1xaQ+Z#oM2Rm=L=)gOo<)-21!bjdjo zQZOaGh<7sRJXCUhZVXbG999%^ zW(qg8{J`1buHH1Lv2B_vxslmCS?VBy>!XT7a{isNatZ!WB8Sc_be6Wu(=pdU=rpzRfR6VhQc+3j~tf7VaTT_+a%n(9g*Py%|+#!Bb8LGD`q2KSxE#qm!Zf=Gg4;! z5J&4OCf(?$&X%JRBEVjd40mKG5KYjgf$2t&gYj%JFDet3X;5NSCEzg!V&rUvLvd)$ zWwB`0KH^T6So_cIG)RSBGgXO;N49}gP_``rD?<5vv07F5y;QKab*uX~7Q8U+IeqQDk4~qDdo~LqoG#Ydb(CpvmYC6=wT?>}6xGX$N+uLTv$5~yfgV50%06gANimODK z$J%MJOYXm&#sNlCX-q5@s%8-)n@;LWLBzzy4Y9KTas!2xiwrMXMg2(P8YHY}koS0w zERD?WxSEUihq#h517lW2O{TVJpy`WmXka)(u%Gd{7 z|MkLz3&U=vpZ%p_0Ta|+!T98Z%kG&K`gL@2b$}dp4xrzii4!z~tx){XD6ZO2K)w^L z8>dyp%dkI%;^l9Jh5{mZz3(X|laFSL5)!!%)HXDdojv23n;6^s&*~lf<{oJ=K*EY6 zz+?O{R6z%L*J_ct$bqnusjZqPy)1eH51roE>KD)L$ilS5&0k0N=ny}Vw~KWZ4}H7@Z6OSI3jq17PJKm-}OzFUwe(U_-QyGRHe9qe2nY#rF+6cKX||(LlA2ccnT*82pH3E zW+fzw9!RWgwXVMv5A-)$oR5v|+n*XYx)K-d^!g$FV1t$oc^D|LFvji(bZJJL8c{DB zQI{yqZ2s)@QpnESMS%c&$;-I)=AveBJ6msUs;P-^rPmD?7x=NMmByPr;XEyJw?IG#L4DDf!?(aD(6bT6-75zw&zs1h?rW{U9W8UFKA%X-x zX)Y}LIb4$(Nm_i(-BNDVN{>&)M3b>npG2SN^s%w**T4@@=&0{>y@H3c=P%9VQofY+ zg0DZ(3$kTjytr6NkX7VB2pwlDVuDClLJ;#q`9FJlH)PKn;nYQ=x;*pLyvC=w{`+Dp4wQj_`K$a$vOhoi=l4ocAmT^n zV3>6U|NAbFFBQJ`AUmH-75&#AfWSpbg>4Gqmt+0^M7R-&5#;56fI7zxa@RN##9npF&nwWvsnoPi52dX|=XoJfKh*7xC` zQ;dLG+T(q%v8LgbnmSbK0)m!~wSRfIH;HdHU#s=b8t(5`_xqUv{^Q{YMZ=hOw2*_e zzx41bWkAPLGAGASbfVtWITn1{(l$4Tdw;!#G=1WTmzl`^@-D8nf22F*8Irv{1ud;l zwnV%~K>}`}DlWX6+kN=e>Co4XAQUtrnnE^aTO+uZhkLI5*}CW9VGr$R!A|rz<>icN zB4NoRxiTcvHAmF!iIF`=wup6vWa2Kc@oeUZGjVb8jH{mtobQOV=N$}ikz4QNXQlf_}Vo&PYscpTNEs#w&NE zK`S+t@~_$4ARQcPRzc2AP68#R>c};wxD7EcGep8hBaP>;1Ekf}htzQEvlwl!T{cB;%z{K*T*$6oyFDJF-g5Hh1GQD57zx3sOxAW&CNo-e7* z{b-G(y9dayub@1-r?r3`446mJ@`bFhqM zMy14b4;Pdu9A^X-P|xobBp;{H(=EFRk=p7CgaOSj7M6)U_FLAV2cDA?i+zJ6gI9ql zhC!$&AGQhq^`V#`DwMXx=%bG@E|JMnijVTC3Tk8C#dX0)YLpZf%0m|wDG>g=7n>KX zifw!I=D0;yY~}8O#$%yj&Cx{d3s$1?czU~l8?NrgDClOsKdhZkEy|sq3Kf$s*vrd$ zYZ>GY7OedlR*OYcIQ&g*Qhn>?7vz8Jh7eq+@0xk0g(j zS;H(FT}xEMJ#a~vcdpvRY7}(dBO`}koQH{W(hF6SGIiyW65RxY z7dh@FjpvY=S=sHCJ4_D9<0N5-9k7zOYPX9q&Mh8Wv4@W1ecu>wW)DZ-2rj3fpn#2^ z2j0JZyWKYpueE1|;=^1BNV zGpqW?sl#RGIYlTlr;=JVqq6e(AK#xl+vx4Ot1?8@W9LY=RZ;7flalI{J8b{1h|9&q z%gAUq@4peej7lM-(a@XsR;OA7?B;D_WAK*;7*p~jQ|&|IQf|N6z8&kf)75DP9$`}j z$>XEzr@qtd3()a!?^i1J&6Y?+?@HIaNNRoH1w+Mi?%18>f{t(!Y2YrRop@>Q)NV^Z znP_T?z#J^%(`hu*aHhpj1xYK4;T-|{zWV)@eDb&ka5p1JJZKUiEK^V8ixp|a*0WN& zz(QinK%0u^FR_@^+S+MNTn;#ELXO<8UG28PMjzz-gc7a4={8 zjF6CUTHhvc7bcf$Np?tDC39CZ4@{ZVkkGpN^T?CBVinw#V6C+^g#SG$&d?$~S3-Oj zRC#XgO0n~7nXuVHlq0YFqYvAlhH2`d`@@$$g_BU%6`@!zdQ{@AAy!%rpE7RMN-s>> z&lVka2JnIb)_8iNqGDQ4&p~TE)IMig_wcD)j>OhQfA5g*AnugmF3yEIb1FnW0gn?T zPQ>ir+9LK7zK##QOClC#XlRIA?cAMDo-hMTa3jh}Vzw(eHEOz-{sFhKsc9vQ@7@DH zrTb*UOk967Tvu969;)q2JwL5n_QORp{fg#ljb<*5tZggv#%aWwm!gGCVYVs%GqUG- zl$4y}b>w#q1u&^jlYw(#_Y;9|_)&#c!{Os+hC|kO+MQ4^@Oy(Tufg}XN2Nhcnr{go zerh^@jCw&L>24dn>4=4Tv;6HrV8#da`0~VuGD2fh<6)$_^ZstBwe9(>k7jxIs+Tp` zZD51?m^ERNayj!*@>SuLoW89OAHB+WIgE+h>^cQyU9|Q`V|DA{+;7Zi1ES+VWV+ z81|vL2)im`ULsYe`y#$F0;~bB)h^Z8lF8hNB*Gz^_r86^3edqcF)4U4C?q1D@CmPJ zRB>Jr(mfEaSCog7K*~a`CzIxFjU{g3ke>i2bDygi8a-FeC{^{#W5p|yJjV1Ed6i@J z`AgH|lfbZR!oWOs4vrI`8YXa9voae`28S^OKsP_%CrqZb>BdGN-=a&RUv+J<!50?rOf*ctY+s0o&WtMzw`0h1puLxyGY@<%5$A3- zATA+C##Jx$w87YsX->iNOyVoG7ppf>1+M%lwa0SKYh592EfQQ3?0F6bKGo&r;U$@C z9QSc+E_ZXMpRB;eN#7l*UTG^9E%*A^!B4z(UhTAO)^2g_C%a!;YSJfk82Y0z-r9Y!64{1Ld3n;`Qheio+?UiF~7>g@D%T_s&PWZ6P{q#UTasBrLb z%v(|7T#^ckE5Zjh?w8?ep`#gDks$*7a~u;7Y8p} z(*2c}*Ei7Ddna3#;r8CVxEYly&2$!eiDbwn*9Jl>MkWWRW|Ih7&ZxeyhZRRy)wYMb zdQKp1Y}X>H#Vdfi_d)_4sk6kI$BF0JU~G@Z5wB~iPYm07SwQo0ur6&(%J=Lt5#vll z6Ko3Zr>#yo*+Woj+t^s5#Db(c-5gd)LFH#eT}bHZ&qD$BJ}cSm~h|=*%0J40}P6sb5~iz{VS3C`|-CbH406 zqkk2TE1aC#$l-=hqfLKq<;s+B03l#mbAJcjiu4IOjT{ZjWVVuWUm}+}qO5S^bgbBy zVIVR)FNrq^?RT~GSVJovD4c5BjdQ+4xV{B_3oB`vA?@geHfnMNt~h8N9K-bubMegy zptkbbn`l5<_Ri9HuqZAfClPEC^}heIuZ}pRM&1I z^}T3XRm(CIF`{>ub;2_5Y|IYL8VN%PG7d#W8w*b-3 z8#aQh%^g$3y5tBAjm4^X+jGN-%?P3;CO)xHrB?(gue@jC{Uin#|Ux-dj+$cJG6 zWh-#*1j(B=2OjY3&M(J1@^@E7jyOE$I>|Cp=~Wja@WY|fax0$?L7k=9+*1bcjAqZV zxvAA;M99~>>8JTz^d2x62ry~YA+dLMN&%zbcWQXaRGChp?S$B&Uso~5_t+hk{o-1) zdFhxm>%D7V)xMW)5u`ti>kRFg2+~T93@33xr?^&sLX#~v$E+Bl__Xwe$yra=EoBOna2V&e1bA3)B*rs!nq5}fLOe^ z_LXqK_hOHT=i)72j{Aq!Nhmd*Mo{LIA}Br7zS7!KD9z2Dbb~R=oa&PjRWQqGg>by= z2D$qa3M{1q&|E^VQtU{Iq!bGiX`tjk(Oxk52$Ak~NVld_aOSrq!5LEwm+Tw?hJS)Z z4?`#epG9Uf^q_yDOLi!T2D88_R?nd~SJoMf0C3wekra$_%ynX9%aN1|snd0&W`vWj z(#04PB*fsDnqNogs`-lVba@DmvetFRJyoLmJZEeSVY39Ezwof_%GQT(Cc7Ox&oujf zkh4aZLxNWN5h?c1;^LcKexN1aM54H?f}Q|TR?9CyCsF>h4#&<(>HrnwB+=@SVXs1Y8E;a{uT;K>!@S z+nX@tE&;&d+q1~}H;Pv|DFm=cNKPN90zchPQsjJkq4mQhb|t;FHT0d)`D+8fL7CUP z-N{tr#u-o+MA{$SaT;aRpXXqH{Jk6E$|V@@EYWSG;c|9z&6_gSlmjzXmN)PZe{%`U zw$ipt!3(2&mD=xl1?ZzDW%lNfQ%+ij;9SgM*<#mQM3Z0J&u`zNRx0AZEoa&`?XAZh z7zlnAO|_Ml`P{>O7EO;SP&X>q02%h-#*Yt@k5JUhFLso~Cilb#&!edeR%d3armS`P zM#kZ30lZ7`SRg;|h)v7(d=71e^18Aar{V^!SP+MnSyGQPv+2K>4Wd{xJqX|yMoQ(v z2zctUdR=duy*fSx#5)%cXp;b5^TpL}30yv(!m*us&W&-yu{sUJiv3zgog@Yv@O~)u z&5(G`1!eX_y-tqg@u*0D+P(lRl6hYX76}=@va)$GG~R^f;S-6G{zsu8mp$IyF3$th4h=C2O!AOVhIfObYQ4$PPf2FISHv}irt zn=dbW-XYIsuyJ$Q(ZJZjl(G*$SmQa=)>*r*N7JbvLEp5=b)@(TooyM0yX|Zkiksx4 z+LyG2F2%7hC94~@&Q4?-9*f?GFb-qJb07Xq zsIjA?W2qI0v3HAR{eI*n8>RsPFXx-z8w5lUYH2~wtH5Un_?+l&JeTlnFRiz`31dMv zO6qMpOzImi!zVkt_}?%IFeOV^@l5w|aY{)^ZT}Q6=r)~u*^-sTVEo-SowAIf;0*w{ zkgz(2l!~vvOKpy3WFF(CC@1N^)cjmFVQ$K12L4d%0%A;H?-JOXEP^QFX0>J*t9E@Wtj! z&~N1WsY;g314x?3S7H(++pY}5_b_qbW>jXtpNsF9Q9ejE#`;h33{KETIpC&{>g(Y? zOOjszNl=5uBtC7@O9~R;L+V>w#TI#U?zLH6MC|EUovh#d$g_>vrS|V1*I&e0^Hyun zQCV16(7N{ju@CC=o+#k;O~eCj6{dx}=<&e&gGl^5zL1}>XqaaUz6v-iCI!u94%9;dVLbDJ^YEUmoYUtlVI4)qfbJXg7P1)yLrv6yj1E%>vc zZDpIE&wjqxTHaOovL?me+ZC{WS)}^R{lnFMS+LK9_cgw>yc!g6zm(^%xqMm$staM! zym}pM%MFwk#pM`S?ruI)az$wCSi>CC-m~h1*|q@{^VX6Uf;H1%Luja%o_>DNU-k7* zUiDAxXKeu#6fyy?zrL%lL^&Y=n1T;!?~I7={(SAdTjhC6pRv;N>d&Be%heR3E%z&6 zQq&Y`8><6-Q5A%Vg-GUCKi^k__I=nBi{J?FcSha|H@VL)ambHB zp>y?d?oBff6IhKrnGUOizKodL+zvK>XaP)eO{E{W_N)QCU5Z;5Nii|xS5$OVzvkNm zK8fimez5@UTeH;f8e@RB+ZEqS*m`<;avOb;X})n-v|+$KU=&Zf$ANkQebx)U*fnzN z-?_|hkzc410 z5){y!xgE7=uJ!0|ZsE|Uive}pWbGCw>62V;yw9ruO7BMBP@fk-pRF7?{y3hF=vZkm zZ}ti-*u^U4ab5*Yq2AdH>7{I%Bs;8rf`NhYkm6OzBkW9{s=ayGx7JA$Q6@~**xyy) zb`?AzM^BID@gEX*`8c;w?+CAUC=)b&eSKqWhMnI)mg*xV9SQqTnG}8#aKTXLV0G7% zSTA;gp~`n=bs<s8nEaM!CBr zjP)9QKuF&LGI;i5vcoB5vn*GlX`+xHu2#{Cv>$ul3%=WZWrMNe=Lq^#ScA5Jy>#_c zZC%3&XD~rV@we}E#p3}JUuskSVCGA2=~{BTN*1Ov#_ij?w%BxojzxOZ-8MIN z{Sq^0zoevHR^ds#Q4@R(?_EFg;|bXm3bqzgpEvL>y`<8>SULv!(KEL0|7HQGo0q+E zCM*H_Pucv7B1{jZ;V2F^;<__FaBc_fRcr~|BHC&|Nt3e`-()C86{+%m3qaHGVp~jv zuhSnsrC?!U5s=Ra+sMMIZA&v8(?70l-%Xa5tgF}Y&fp{Tg1+T}G8bv?xPqZkYn8W% z)_|ih&j2fn(C{bm41QGT_luSMkQz&?T;&`Lz(Oa*yCz|6Keur|XU%;scD} zwBC+R3m(R&!BcghE5XZM_(N;Jn$e8oK%4X}Y}Ut*(}Nl>e}iOku3td##WE2ZZlES^ z>IvwVxxJt*)~~8fl$7_R7ih@}X*8m%y%!{MdvsGug(n03z?!=0t!iKuT)_(yolSca z0xY1MzY7TOuXdQHL@M#=5de42Ml!z}YRaFKb~=%qu~kc>Yp_`y|0)qURU|_Hdbo12 zk>L#E=ZTCizL9ua)V-s%-cG0W#j)>Rl#vWt5^fEB<}MwkVn9mSz|Hx=FnML{z=DpZNq&GH-q;FGNKvL5*XI=%uihNU$;oNW>ElKTWlLj{ymR~2KN4cd)YsqdGePKX zP-a8nPzOq1uvP&u#l}EmNnhItZNIM1904Qh# z9E1+x;4)ZoLcMW{ccmo3I6NqhC3AW2 zRjfqt$Z1sgME{&Va0nn8q_*dnI%I)xvufBG!B5c3W3~)xJlSQ~pDPk`t49GUmE-DA zLNnfE@-r1zLAIdUK*wAxHdzNyCBu%r3ACdvM$`1tZZ;8krlJeOGD6X!3rVv6&{VI2 zJQhBf2^=E%Sc+dYR#Y&N z-66T>7t$&#(c{Z|1OAA4j{4u93<2kEZbquheaq7qBuc~MNtMBzGNc{oE zklE2=n!bz>pKDB!BI1(m<}-s78SPyNplf%7B=g;T`vTHgANg9t^{@IJn<0DG9QP6Z z^t(+4W+MfF9ymY!tJzhfZ6)&qLCr*yn_6KuZN+vV9Sf-t`Ike(Hg{Rmw_h28v59cm zEr_rv1#Tf33iu%CrRZn225{88PubhcYHMHLR0O!(ZM(OpqUy z2)ShZz%2mrV+ccXT+c_XC>o@RXL49xpmH#rtFd`owol%Dgko|;`OTT|^N?$@(Xwrb zUtRAEh#xmi#gxQmXr<|NyLtA<&{<@jEd~JRi3Ha6sQ#Eo1ZQy`qvPT=!a)IZfAqjA zaGm;IjRX#-u?+*b7c8vpqn_vZ*ce*1=>nwt!yOhk=U0mT2OwQn&k5YC<#MXxB$I@- zxc_<_8`0$oxPajr5wKnoyk-R(vRj>FQBZR0%W^z`*X)zHv*-v2xS@t(B3t7UX(Yba zEX57?-Ltt=JEA!_JGNyE^!)mv;RYct2TZnu;+FgS01~sk$7aclzd3pZ%>Y`(!is-%Gzw)+c5#tV^2HbW#ljSqx77L5J^^e4CiM@PTWoZwY+JdjMI$jM>XRvw8F>)HvXj4gUGcmWvhu}%aPaBb zc){Vs)cB;?7qu^rQ~=H70SmwdK*&AAD*jFE<{oCs&;z%{Y|QomHyfS6hh-&lsL?3A z6w5k$K4%j$jMNMjria`9g1j58<6~8Wd}C`Xf3nSQ9Ns*1TETJ?s1|==m{@Adw;Ce& z@FW1GzDLPTcrjP@Ib=E?XppbXPi8-yK|`v|FFZ(o&5Ihl?^ivAztv0?)rCavBxJIo zIG48VZ(UK}>-5OO`gymOUuCSgSVE+#T_jj=|bZD+HK2AVrZca&L?~+{^vufILRe6^GnpCJbFO zo9L-HN+ZE;4`m)md(Kyg7tak7y13l2)TIpa4k|7fPaCZ=EiZ`DUjC|beqCuu&ZmWj zy8X!5c4j;>wxc8@B)?_rn+5k)eloE|FO7_87)~~OHjA@!-%*|9m8sc>LK(?}b02G$ zH2woRDQ*xteZNKZDB2fA#iT`tx|@I~xsR&*xo`Q(9JZ%&%-t<)ue{)@35nURXy)r3 z#i(*;?tNz|qEupp>I}yr!sy;nD3joLPgSpbfjH`!I_2`gRwpCH9Y^Bahco)!r_G#T z!^>0GxwyHUpR>dB%o{f-m~)D2-n6$rMhO1Wj=n;zht%gH?z~V)m(DPYp)P*?`t)kZ zFEBP@h{*IX#I@qpAKA`M%kUsvePi>cKf)sfU zjT9cNS^He)H#gf6%FVlc?u&TR;VE`J|D|_t(WTK)tg51^>6?h=gaN4C#cG+a$vu|n z?Oro&eR|Rt#{9F46iXr-(aGxo`1Ucs80e?rh_0G0oMMZA)mAod=6oGQbqy%gYY3V&!Pf!DT?0xs z;qR(Sw%?}PYnjAvUXlpfPEjXs%H}7^-hD*-A^F$C(1Toh3)dt*b6sRCPToW*Io)b# zq9#Xn7VDR{p~mc<{1xSb{A?CNqA0}<=Vr?awBv_ST!yzx+vyc{;{cu>_HfVUAN z?Fo-?q-iNr8z^eX8xRlC_s@~9*FI4Xj_qMtkXKTIn5FwD*?cB6G~P_CfOyWvA|ZDR z;O31!^7#m=9C<#})zLpECw~t-h=!fC80tultHgw*=QAoQ;v}I?)_EL;9NP1~>U<(! zC3R&bQv-MIMZOuBh;XD@R;M75Qihpyd>;=pfk`NTl2aiH&@3E&;1-~zBK#dI0B!l9 z{P}svmXV3sAn-ZgGi~Wvu@=s zp_#thJDTm&I z7wI+gYu}Jurs<57B_h2nYXJ4$EMm4ua#$khHF1XI%vAxmpR)i5;3gfg9!t!hedV`n@GY(YF-wQ^|szKF*vWl@7- z;g4|x5Lan3I#qNIsw~g)+bScChY@f<*VnDQ0Z;(=U5u4l4DJ_JE_S5&qYf zbLg&{J7cAeYn&20Pso(OXlEsJR72 z)2s>W0myeH|6R-C6)B!L81o5OZ`A?7T8jIog)l#CG^map+3i;{rzz(ZU_($;YstMz zuX{r|j_T#>a(QqIhe%F(YV&zvuluLb>`SQQz41u&B%8XN7Y|LZ86eu*odo4_vgZwu z&1}SZ?}Nzvv<8kR0A;K0Bg*HB5S2Sv2S=xw#Gq_`n{+|IobexcYc>j548Yqu`l)KG z;Z(IoK*Q+h{1oAs!v52QJGHDNjQ2C5E|zGm&eud>9n$%b*%I2{p+6Q&6U`gm>6j482UAh zmziDP8ywZbofLb1qcV${M@o2bl*CVKBM=GnweCfNi9y377u@Cf#067^b$M~eN;VFku`UcFn$wVVK-7-&ECf6XBY{QB#$0t^ZXTXAC1ra5x$0MLVq!X&)_XuS;=k>8 z)5>FMZs5P1ae3w+&UgWlqMZS_q4)Y?n!fb>j?@C+v6W}k+6dGNdsBIHLNIMCA$itE z-&nAci*&zBP@C)H`DS&}RTi_~A4AWiV_G&+Z)0$@Z6Dmt%8(^-B)L&{;n ztkaF|W099k&((cOo%Qib>{n|vj9KqzkxEdh{qu;%qw(fmhx6P-8rWNQa0FI&|TEGmA!u)E&AjIqtv@e+OvN|G~*dXtI(u zN@@!;>j$q@lWHQF{DQLjMv%t28=^y$o&B_Z)L87}|M1r>8 z_Yy>J;M^n_CKrmY?wMQp0slf6J#)1U~Mr^Dh!NZV)!X_>w0Vu3=i_U{p)>+r1$2=w9%^aW7;^GvCSZ zTKwOJHB7*jH{w4h;F9l>c}`q@sx;7KCsXf;bcLb`vj8CCNIUZz@G+pitGa|*7<_x83FLSf@cM|809eBXvMO4L|QiZu|HXaz`b zid3X^4j3_FpspSL6nuEoSzC`4sj%_4)~vV8b1sKqkWp`ysVogi%}lBuGuiR#DJ0E6l%}DCPYk_IDPpM@_9CRc?cGdkbop` z+1kB7MJ)3LWjG4Dq{QZuq^Nh0fsY2lRlqjNm?f%#ApiKr|1qR!jR({fL-*VW~?6RP0=vU&%L9HEUtB(Js~7!t<(w zevlcfT8(}cC4UL4d5dkND*nMh|IK^SlWH>%0vY2ghTjNx`7 zw2+PC3B`<%j^xUHG&uOlJ*HG+Y`!bWZ5yFv$6`FhP0?fnTWvCHAp)g#0r0c;i0

Lv2-Ls+lgu5!4KqVgAU<`(^SFudtJ?FhL3oL!YsTKWe$_bQM>kkEIYGDSj7 zHasF>$}z+sYVZFC2`l|yRc9F%)faDJO1cCQL_!)ty1PXhX^=)jx|^Xvkdp2e>5%U3 z?w0Ou80wxu|Mz9O!Yf4Gcer+Ymg$^m z;Hr$OnEZwe=SA1^)qKOH_?uu^tW@bwIJ<4lZB5Lt*39{2m*e2vnQ-kC93pm;=Dw)q zmrTUjNYo$N|D_tH0AK~_zPYQp!+}0)23~cttAd9!6C1=S_wy&W{zh`w3pMrI?gsIw9a&e!k>P*_vIA zeUogGUsjncBn`5tGP`YJn01N?Elz5^-XjNK77iR==3b~UpU^7RniNA`vG#(Moj+8n z3phFC=@kQ{Ao~kwxU`cYfBc63H114*mxyU?nam_1SzI+~mM)+Qy>gZ;R^%@}cMp-M z3?s8G={6o8HCZker4k$J#(OzQ0m`qPCL68Ez}K7H=$ZVPj-_ywD) zpIkBzPo~A!LNGMB8BzW>x9JM0+GQ(pi%de;HY#6zn1Yd6%y^=Q-MjHQUk21JZpF1% zN#Q@*KNW<&(~^~Z@=|o)!Z^lcR4Y-Kf0uPL6722nMz~ZbVNqH#erueY>~=+IjYbyR z7d~cV4pY+1|Lrx*CpM)KeBTkUKgKQ!yK%`pJ@TI(d3*OG!kax`R`y`ygd}AnobHn7z>e>Dm0ChRY`u%P5 zeUALM_hd$QCQD5vfDc33ymhdKqHw|p6g-h==rNB^hi-_VMDg&4 z!)l(quQusrrlzOyS*F*jK^`o*@<2ksZe~-H)daXK3vM|wDxTNuR+`+rrUO z)sC8z5%xD;KVuRjGoy+rVY$mtye|oTYh0(oRg0H_Y0YmW@L2P3HU?sf#21rYl<5sn zF9L#BsFaluKszjlVU#PnES8YDs@crww879F%zQT{2gXa(fhE>KP>@HR-gYROkaA0YPHn;5Rz z%7;Ursj8})Y)*ufCj+$U-NhDAL{a5Yf61u7PR;3ZO3v~|89p^Nb%eGM*L?BUv%{mg z81*!Qtb~#N&UU}W+3?-t=VU9>2F-%DrxL(}J!mWNiDVEK6MM#c*F(l=c?ZYYkGl5R z=nkG%)xC#K)78bL71%*{b_P%emS|#(&+E+)S&e-IkhaH+ZYP3SGPN$XchB0|+hMtx z*wP^KS!*U61c#m0iqx248~Y-4?VWfC-m{{aICu*xfVwp^$Hv9QRjXpfPfkuw7YPiT z>m932L8`D^42XNfA?_|rb+z9I*t5I?BOk#t%Ov6LBN-wqBjExqqgmq154ZKdwkBg( zTASSE>~KM0kJkG(Mpc9(5`5DEz&^Ert+B_Hzf7UPVkr@&oXacF!wzGv5~$ETyd9rC zBW^FrX1@AFf-BbSoMreI|CG1f=7~lzp|v?3ppUJZes_m{)^nzirB7A5w|>X!Ur9m_ zP!q2FXqDfU=4tdq{!rgx(k+(E=S07YYMOQ-O0su5mCxa&94w446jkx;th)KB3xiU5 z?RatHp*xfdReSRkV6QsdG<5Q-o z3<9Hc`Y*K67RLe{Z-!zbF-GBW8o*odON1>|Vl&JA5E{tG;WaX;mX|>W4!Yi=y_~N$ zU!78~zuIpM=R!1Yzp*P202;z(*qkNOsPrKlKr zlD_s2x?9Oj{BvFh#U92~i~8+P6>G7dKzxXoAb1iHUB^hH%KWXSu%3|jX2Y-i;GiYA zv$)rxKc7=co?;t>s_)NL`g%@Lil+MQ!#XQzprP3foV8mIaKpUT!GW9W&jqYe~ zQLd(95$=fPr$IzV@&*uK*ayVShMPamGYKlu>oNxDIoU6uIUCdzRb`cvH&SEhVxWoa*Z;QTYrC>J`C$on-*!TTDTa~(T+-x5x#<3DOi=DHSjNeu?P3@RCDi&sTSmySSW~^ zBU?O%TT&J}$f*swH}s*5(BxTm4nW`N?3ZHjV&s|+Lyg|Yz zXe5(XWFbGZE&R`Sp0Yu?a&Y#>x$N(_Ocw-eFaA}{>mWcie`?KNvE2hSvp&OL%}h#_ z(G9KH%e(kQOyD>>6 zUuP|rcydmw0sR>n5<-Q^iAki8dnu@^5!ijlC_dDd|LBNhYc~ud8`;B!?%meF;zf$~ ze2vn}80De~iNE%<80*C>8(S(L7!MS**VCaiLJ7m|UM4ikYrn;i9Ed=)-#wh>Zi**> z{0Qv5>U#_%gUHuu9g|Owc18--S!MvD8Ji~{$_(Mfcx#Dk8y+(t`8y+<+U5Ry<-lRW z3zra|nZUxcjJ@p^m??PBxS#H&2`U)JPZ{DLK2^KJV<%7sJh2w$urzgviTI!pJ$$Mh zMj3{WC@)XVlGl+>vj7OQnwJb9n5kaks+IvmfgS`SW?0$tj-YfUy1&8XojD?L<~b## z?<%zxdZ84p14D6hA{u&K{blWaJmVLvJ7XKx@Cy(9%=ebWe5IO@G2%)A7$azJwn>zS zo|tKKk(6b51{Xs^ih(o^1Ym&bXwPcd_?rdOq{m}>PC-r`H90B!ZKy~}w^N+Bt)Qe4 z9R;_so24yulS=lkZy1fQUYG}k;zco9XZx|R8c8u9c&2!`+PhD+lOtxW>-1B#WotLB zz^WK`yfYN(OqbI>0{DJ$#Ex#VI~)Ab#KZ|2cFXpght|YH3-vSyFt`U8Nw>!`zSI?# zTGDCjhOKqHwmrgiXE6uR{%fg}94L6%^;L;Uj3`!r2Rbv1)|Io&Vc^chlY4wM-V|#} zCSpMw9rzR!hlH8`FRWI7nR6)@)!QowF|v8b2cC>p%L&e8DjNIdaBB|-U&Gb> zOSq^^^0+-~!mfz${q90Eh&(11%d^%o!qiS=r>~PmNx(q;H4U(sqjuudTglD`{}2a3 z)H<~I8^|~TsK8qGfZbgh!IaZH^B8vk`5^TE)*dVL#8f-XFe|kFr{bTSYIE^)r1A99 z^t#N6bAmy&^zKkQ6W*W${*8cS5P$S%SFXdz_@Tig@BPAeIX^@jT`uYK*p;7*UBN%j ztEixlmzL@_MW5*Jr~k$uJ^u!>Vp5aqPRo=b_dAQ1Miei$#^pJP1}3S0Gze=FnT*Ok z{5TXbATmIH`<%xU#~FfNivj306@Xr=HsgEc%#7Bkbmb{7NETxX7J?*e5Kk(@bDxm| z0pY_+&>}u(N@|Wr($JAGjASZBnXx=^4B0u;J#&H`p}UUib(F=spkgB zzKqm=^MqPdygo?q#(xUs zedeWT{Yde%Nn{W`_ugnlIEP44v^`wyKH7S8II#FNy?1mJ)$$GQW1eE`GCfH|R7%kr zm1D-bI+h!J=a|B-@gRRr1uEKGf>08)JhnXgr_a#^#xZH+3Y1O^@ov?CO`04(hDP4d zpj4%{u!B!fs#xuRyll-MYsD!%Z*FSzSPrS-86D>AQz>ON*{up$dVJDSsTdotsDq;b zEuM=6E46`Z>q#AAz+ISGaHKbB#_*w)Sp2^&aUiHWhuiTt8h>P3d*;`xd|`P9OrWHCQVQ zH)KRk2l7v)1&yP5+#wxXI|Yu7DVX#rE!8m>^zBV`*%s6W>^51SC|on`yg~h}1GZiR zX=@V|YBotDgtq7rkKcgKd-UjusMwk+Ea!Sv@xF;N&jj14mQ9zZ- z;ym22iW5lV`M-`4q0==C_UqEM(ivkjaMxkZ6(4)cNU zEFhTS3YA-u*zW2W3iopL;ctYVhX=)48jt(jMw?H>!O55brd>Sb^V&zsLhTEt_4RGg zq{%!#rqLh>HgZ-K^NG*C1)}-3?J8#a1~cn?X|L-~b#+!oBD8^&9+A#9?nnl9Omz#9y0 zjbPUcMiLOh3mjabJ$8$0Dc@wYo(AZ>bTDfXmm8X{@0exJ2#&0>V(!(1Ak5una zl-Rf7lu;A$Nx3tI4WOOvp}mEcTPe{w)muUfCif}eWQqd=F_@J^XK&+%a9e*!nZgSC zyBM?HAbs!HrCPw|(I`mPcln-X@Vq5%1sIy0Glc7%wxTpL{A`CCFyHS|U!rnr5VyxA zD1Pa6*OZWuSc$St-p*1~xMDFMC9&Ky>ggTuy4Uf@5E`x0;)&uIS8)aP zzzkxor?UV+DeDGukQqc4m_az>l2C+%Qf@AOVW2z?1)HEtP|?!PghB{>WEWX&6WBTA^eCLVmcc_j#(x;IbeOqxS&;r~faw{~#+n z$a)pZhXk0>;#@kKE#YDmV%!q(k!cJ`9a)M%R+2b1?gcIj;mPjlY9(0>SV2}?cXFi9 zI-Mr_mF9Y!vF* zu~2lMsy=_26s-5E3#!yyY;53l;qDZ3FhFm*HsDs@M`=NhQla@l!?2})<= z`*FjjUtlm-yqXV2a2!r`Zo!cfw*hMsR3?YD6_CWicCuTaaxwA}K4G5lxX z;Yb47z&!(VcT79Uy|93z4g!3~`9>b1->JAr;e1>KO}Cr05e06_Kjsh#@_!Zq)CpD4 ztuqZR?d)`;d6Vl>8lA~q%jZis<`aDPotdPF^j?ij{KVTS&QU8Te33$-gt4demRgwC zA?l><4vlPHX*$#c4pXWeHyoGE!kz!n;3C=rZ=>FMys_wbJ5C93M43;ZG0N!eTq@vA z+uFoarMd_I93xmJuA@TDtz;tgA@qu3*TmE>7usg?xI#;om zj${yy?Li-=A?sFJ`}&O6juL^$V3?;b)IdP+=Xyf7HFy{cEKv&{>yDn%%1`xV45J?V zb@dI-*>uLb1+B!^p<4`T;xk1EU%PmIH;ZP9_-lMmrp8O56rOe<<K_W^eK+C7#)hw(Zm^tE1Fw#0E-=P2*xRZvi3BU7h&xg`oE$gc4h%&+*% zqGGN3UA*t(41o_92dmWlJ(Xz@U)Acz@kmE9p6YvAP7wbxBXUJTozbh4zh8hTWr8d? z(fb}&F;XZ9i*ta);6Oqm@vggNo6?%r^U+rfvEj>s0Xp*Y7{#eWNi0&wRdXK;lYyw` z7K3s;u(I#f1d9~lcwsqm!sPIu+2Yt_^HZwIewrq1K914iW>6&$Y^Gul;B?Q(^Mc3( zZ+O|36{>TzWC7*BOxl0yG9I9On9q%3Qc3@$QvWM-@UBk=tlpzj1h0I#zX}qfUP~eAP2nPN+BM{)yyeA3QoD%nxk{MOws5BbvOw}^mH14fP1G$m|Ti>mZZ{tV84K>Q;QB);XxTIdT z=2n=q1>h-DBTN6eR_qkksOR}MYVtBtEZ%T|C|D81dlW*!18Gp zIHZ9%)eIWHLS7dm9gaOSO~~8^Jm6mGanFJ;UjEE0j#-L<0QOYncW28I8nX^ zXdx&)y~d0@MaANmj?+L}h5H?@jIY$B7PeOkqK6G}xth} zRxv6;2AQDXzgDD28Y2_bg!8c%f@F$(uIKCU&41YsQz|-FS%a_CFHfJA>4g}${kpeP z5YN_O7w&(N2lEU0`Py>g-4(L;!tA$YpBi@!dU|^|tBr?}(ST#(*06UwV3%6Ie$Q$t z_s3W!Csb|VdA9UZ0$!D(ynN#1xSUhAZJ0%bzR2C3Rqt4oW)QFqwz04>vDwo_s4NFZ zCwh4)U-oNbt*%y`*&EE<04)xuy}YhHF;U_X*Hp>~LCfnNxL7T{MwW-_GJ|_|fP=Oc zh=Of=Yv1lFpQjWtSXiLHL63{wcvVml7;9A00Pcq~G=jIfvxC&w<|?#MWl@;? zR5o$&nPT`0>UU8Qj;F=Li4eM8cKiD!1mC5Z0`T4IaJ{Qk4}1I-)THcgi$zqfgjc1H z1q^TAFd`m{L3uWOVsbe(z0C6X!ve#_Oc%ERhmhzAK0@<`0@~0e@ylBW6_al$L#>kq zst%_SQfHc$^l;A{Hdo?2N@2Ji(Y@bXV%diFk|nK;*xyl5a)cK%9))z7W=G_xarwLH zi`R00^`~mC4{TW$4{YK2cm%B>)8ugW^6t1DgFB{V*aMZ0KTT3<^PZQ2!oT@^O_W&c z|1gSU9QSiYK!^675Tt+>C!3%w<|};@%6~)X>zrA;e+ru*!9Mc+-*j*YI*(P$ZA~?u zM{~VLoj}y@2xyXcMq$wNr10LpQ2tNhNqpM%>%x4?y#BP3*4!e_D6hlY;J$oelB>#0 z$JnanbAyqzoRAp#F8MU;AN^K3nBA;X(xrI4b4-HACfV3F87{NWQaP}A21WX~KK72b zv7?#beER~5)y^^Cw+C1dg@N*+oe~_jzP`KsVI%w3Olk|oWuzPT+KbO=T#|a@<#t6 zURg+jX4fp`&z`8i&?`dxT? zc|m>EYLpb{A)L1T;#%DhSJH4{%zz-k=<<-dKh7zOcS%XU@I8!<&(zGUH6e%)NwyeW z7?1mR3LNvyXa1A<5(-+Kl3Tnz1pQ)2lrAB60YHnPsg|ix9ygO~HQ#ITGyw0?4L}M@ z-JEzE_!zK=+-y1BuLe~U%kqGngS(s$BXk!h1UshrBLLH~Oc(n4Brn;R=9lqEKcc0E zRX&X3>C^p+rhg6?DJ_?9`}DTs1x~~*#5ym+g$7w(!K+L_q#CVpmbd_fLP#5~cf{~z zKj1dmUdexKCha=2uIZhKh?2s-Xn5?jS3qz$LtyEy%OPil$G7<^3x5@ZvM-R7&aL@_*Utbn zgoR_*+I>>oBR8N z8R)*fWbh6-w^yq+iZLhjbGC1|MV$B*nrK_xbjft!sz(@~`400v%EYd3z%-yHTMNSK zRsq^k`7se;3>-LD{!eagMJnfDlPkxKNbE=+q%&_ZSM!`wIuY(sonWxBSx$OlKa`0z zU^`CaQG^E#N;VF~|B<_hjaldlHH=3oLvOl9X?=k5pqXg@^yCYb+m z6E)rmNan6!VYOEC?g;hi9LH?BRURDHHaB4_S@}KV z8~}x>wd#N+Qm~y=5Zl%NfJ=8o_xSwW$HQ%}P=qD~dC1&0#s2sWP%$X=<;ARZKp!(; z2Bzy9?Zh#ZANuicbTC7}3mTb)5s&Ge7}YB6-K|Ry%6g zGk8b8k;BCm;3VE)s`Je$Aw_Bg{+p``Rg(3lTULsW(Cx4qtIiR-qCBq{xzztB`|z#g zePsdJM;j-}?eggYX9p|yNUi?fyceH?FY9F`#NugSnGMJ?qhAY>iCu}5F zKY08gsazVD3tu*s2u&II#`h{eDK?|d@n*Dl~C*ga0}OK;R3qdTQ=@&czW*}aR&$UuC_;`K7khg)OaB_ z5^p2Op?TuJ98CME_b}E!9-k{ku|L!F>~E;;W|uAT>r(UMYeBwopHDT;Z|`~vThTw$ zF2q|Av2=r$=kFnf4c3m1o2?M*6^z%2M^h4w@-dXZZ_)}9a|_6Vz`nb@|G>U6bSS}@ z(kbjIaTI~-KNlKoQgSqHZzN7SdQ5Pz0ic(;pnh9HQNey}BlYE7prH-Wx%GA^;q0Y= z<=pk#bZLw=U*F$ZbaW-J!_B|t?_zrL+@g$HF%?OC*WlkCO~a(Uq+fArszw&B zi*ssC`QSgDVQ4ZbiMBCi^^S*L>ub#>iAsgdC4d=eM=a-OY)L#W;0e*DCxH-cx~9xt zE|GDN?Mx4F6NN$QX#O{~bFI*Tf|d!DV32uEIBXB|O_Nhnhl!KEH)Ml!a?ClQP2=mk z%X*(HqXsayVhllhXWq#2Op>CNz+5DSB;Glms^ls4b!$?N>nQPW!;D0=_4e4JAy~_j#`}M+!=;X4w>Ht=6YM$9S?eAyF%^J zIfD1LQ!HDOTRW$;3oaX^O8){^7;N?B_%SCRm+xWeF{!J_Yfo-dobQ(RG1!0A*(VA= z>ZD1NYh&&M5d5LW9mbR^+nSqb($BT9xW9eV6?_LQ@c>f0=f{8dy>%d0iDduxt8Uc| zfJ;cUTJH!#lqLPAv6R6ALPgXQa=F5dDTKiW>#$lb-~lZZzW=p7q(4Ft6$4qN2L-Pc z7=YVAFuWVJ>Rc}>m<_-Gi)~QK@N%_lBd=Cj2K=Rb?}#2Sm#1Ox6uh>sjl&cbnz7Br zo_Wec80Wnaph68uHq&z=sFv*FZ!1Km;mHkPQr!Y^ejk9jfTkj*p3;|b+Hrrbsc(C> zZLO@HR;hNs-j`LexG|Pna<#y>;4=DDzpBi5kzifHA26jM2XsyGN47N)tw?doYL8HK z9C>A~lA0{$py{-V3&cn^+Tah523*KnDtNsyEgY z+79dPS#5cNmNnZlDrtlNEm>pO2lR;u!(lc!&01%F`-6v=X-M=MsR~u=u+kKs&aTt5 zqZQ}iwQC?=(K@_D%zyKl!ga648@65Vkwa(idc}Vx{++69h(T^BV@P~d@XzR1T&+Ild0j+EoEGLdoP}r{|M1_>rwW%FRIg{P1 z&1V&A{M;$g0rOPjnLu4lzUrkXw>#qdP=%72D+BD?fe=*C5(NIpQv%`@-z^)HpE zuH|+8$RRfyDssro1_7-GiTkt(j>!N6PWFM2+$kf@I;$<(wdjqKvH434#6ci{k>-Ff(o|fj>ige_L56#yd(bAE z`>53LlWpIE&LNd=kjuUFQrzquhl}3UfjNdP=j}y3>3Sf8KDLf4QrIuC3^P~#sBm-- zpKn}5`lQKVS#zKavF14j<)!p?cXdalLaP37Wi5c1++EyX2^Gq&i^J{GLMBt2ALJ1h z?2}G_f?q&U=7SK}!ynPeiX%~qt5i70`5=ua&X=`iBV>gbaE!G_Gm{0*UO;t5&?A6G zdlfV&6a$SGd7nnUdLWhhDJlr)Pzt}{yz{2f*I}+m`O_~a_a}6CB;fqFUv6at8?2%7 z+?Y~@g7e1#y`R;G?y+wa?i<{Gy&ij3hb+ML%;l#%S=>9WG_7xxPyt;?(u4i}eRc)$h(4wS988$%FW_ zTTbLM=`9U4=R(x?bCF2vJi8aKqc-~TuxEvFC0&B~r?D^+#+ovhs?>gfFK=}-jM6`U z^%B7<9YCGz$X}rm7Qfti<=~x#f{MJE$#M-FGA9S&z*|D4+Go4e?Q6z3hE}I6Trh&NQ>YNdPh_n?YRcHHf`w4(1cLa3bI6GG@Z!lFa5@sqGy&%0=m%zK* z&Kk*3G*0ci&)}==3FI1vqg>ziK<(@psRcSjm_DE5UEsCx;oRN&2CZkz{MQu%%|V)p z?s=y`9U}E-O0u=(9i?p3kuLF&-x{)BZh^M8wOu_uQ35 z$0prPG`$JppWpunU^J}es4l?W0*OV=@qdu>n+w2t=02UMsmng7D8V4+h67sdys*2& z%@Dlts`TuU$CG`*{-Y~W?TKnyY=Lt-CgS|yGZN?0C6QytbskM4&lQ*)g)&XLy&^){ zo!UrnS9Fl*ex z#07@K*~`Z+jhz8-dP*%g_N{K%li7nkViui;N&Qt-1w3{Kk=uirp z4zmTvFw^20HM?&?!<=lGG1GkEyid@HW=WyfyBxxwX4A#oRX8DZagI&;TbJ9k3Tu%B zxMjakKsmo+!CI}I&nE~1knu)VeivRnUOksizI)VN1FPj=>cQO(UMu%JgppN!hb-Dd zJXmF3fmo?dymCgDIC)hL*-Hp0=|9N73N}*yh1(nKqi8?6DPAM7aUl_%K>CxPLwf|$ zu0qZf0P2%N!5%h~x4~b-?8>oXzlL-P$={-zF2-~GwAA-+?c5E-wqboxR8wo&;3`qw zv9yipdH=my4HQ7Ssv5aDG*}z&?Wm@i(uzxE57evro=*T3xoD~VHcWW4eo*=cmX?Il z)B%f`tn2)g2$PfTNpjg6ZFp`N&8HHLBS~Z3A%jbEE1lrrT9?GWPJ$_5GaaNQ&$rh6 zQ`Q$KB3>X-4cCha>9yUM7#_x&hTMVk(M_Zmw{PMw7RdnD;uMa+NZ9Hoe{$)CKQV@Q z#iqJvo%A63y0d&G--Wsn0lW%i zYhWHTB5}arkd<;i2aOsR__NYfBh=|0x(6{RThg+v3ZxvAxL&0HzyI00K3));xVZ`? zpLReEja|f#Xd|wKWirO|!RF`6>sofCojJlf$364|Se`bvoYm7V;<0?njj`wTS1O-h zgt#2KIGYG^-dP{ZYIW$_5KJCr9E+#tzDdKyU_%;op~WKdw8dkoQXFW4if_u8{Nw?r#mDn3YI5x zdmp9njA*X@XM|3Dq>OjJdLrROFQN3_O2LqmXuXL@wF}@FkpAsfZpchxH*{tAJE?xj z^RSWYsLAfpfS)pYLUcz>fECqpJum4s_0E26otc*)h! zly_0i?4K9f+BYGv^X<(WL`0ZW!Imsaj(cqEjJ%YJ@0-hh=~21XN}Fz1F3<7V*z2k} z!kspL`{PEr%3b#uj3bd2cWLUzqlvfZ zt{H1tzc8JKWGXL}JJ|ft-7pB*3AlMDp<#S|CK3-O^-K*$3ZTCap}_cfy^)7UbdIhX zo4vGJs(ZC<1vZ!JlQ8Zt9}>5ZCH3*Dep9N~&TZsD^X;L|%`#T(bsnlyWz>w`*HP0k z^6W}Fs+UE-+C=-F!@)rd2?Bi(&yQ<_mw?lzw>fm7+^iRBPCm`pENwVif1qyQy~APj z^JjXz$&v9ySoN!-kWh^DCdk2DvPQyE^sGL4{Xy>bw+egGNwd(F3KJ7kx^rO)hZFkQ zYE*l|N{KX@$j>%4I=?1rr1z!CvB!FUTKNYyf=^AqvJ!$K!NV# z#r?1^E{HTPPX4=hC8yUhDK#gdI*X$*ceo$*XyPKZL}7E#e`2Df2IK4)bXoYSa6r*(iaW!8hM4#+?PwMy3w2J5X{u~8vW+`J-oQnRmJK&8;&$^XcR8~E zVrE9x@m}!u>S#^tw4_O`ug}pw>C$myT=Kkoi241JA9qg~YU%c-oQJXs#$;<;(zdjZ@H%F< zH$v9(j7zUk^>Tagoe~*HS^As5S=!VTg(ur>NlPFjqm15BMiS#od!zrpUOQaTUP!5q zO-~m@C&eZf+iuOGuXBD}I+&L?H~OZd=^iK(=~E@F4U;>8LaMVz9kZnF41blzdZb{ z*EXEMmBUfY@zWw;`q%>3B_*UC!9>CG0H^Xs{`(YWqeELmc)}}$!REP!IEP+J(MZ8R zUmBs7HDr$<=v@(RpG0JsJqSrS1L` z&tlgTrS&3&XSgVHUYLwFSZeOo(X}}$92~#?$APGWIoP+D!}cMDmtkb$VQL*bV1LIU z(h>T!ngxlrB=KKgFyAh8d>vn_)j1nlJ#)5>U5`(xYEGWn^&KGiuzNkd!hG9c@@||) zJ+H)Nn*K~Se=OF|a#8BXcc>%bmKY|XyF<`}(7=Q(`21~+(^cv-=c>*4i-ihf&BszZ zD&E)F{cG0~WodM~cIQ%J8X^m2vY14_=()IQ$Qz#?W-++l$fY7ywPJESZscdjrN8@o zh*>=HqhXn(|J?FG-l&vtwBmU zm}tsD0)sXP`_J>clo&evXk^~h^ub|kVU#w|+#<$`Fzx&|&y>*1rO9X)N%^Th-k6;b zX1SY*=nM}xPbe_Q%$sdGs4?gxhJG!d^POBrA|(v`RTs%5!`s6E=jmVL!Y;P$X=Vb za4dBMSq0VMdd|hA-Zx)T6;UJM{&|m>a5RK4M258_^_IV^=&VQrc{GGC8#OZH!cQ?f zbqu3=VvEwhh@J%&e`*ds(6qFmoVUs!CX(x-(-0HUoVL8f)l^N-N`K-GW6LmGQF%vtz>~XD#6bKjXsD z(ZlfB5^J~Lp3vFDT})R$4%}4m=a9=GtQHWnn8Hz!;2!e1U3nMneA2y7jM132Z-+f@ zrGC2utC-l(z&lfEhhRS2(EDO@U=W4j+-JdTdiiXeim>nDq)_gLk;Ra-5W~=@^ru!J zvJHui&ga_<8)v4i5R@M?Lz02HUj{VAFfwpQZqy$x%<2+^M+5|&o#8~q#9{B>bDS2( zN-pIo65zG9>AIedD%cFccXT>zw*O|)%JyX~SxRLlyeC{;t8Y!b^HD&s8QhC;`+Exj dKb49uKfXZmx9cnR-G>5xNQlXb77Oe8{11;NO=', 'Output formats: docx, pdf') + .option('--output-dir ', 'Output directory (default: same as input file)') + .option('--output-name ', 'Output base name (default: input filename without extension)') + .action(async (file, options) => { + await convertCommand({ + file, + format: options.format, + outputDir: options.outputDir, + outputName: options.outputName, + }); + }); + // Handle errors gracefully process.on('SIGINT', () => { p.cancel('Operation cancelled'); diff --git a/src/commands/convert.ts b/src/commands/convert.ts new file mode 100644 index 0000000..f311c7c --- /dev/null +++ b/src/commands/convert.ts @@ -0,0 +1,206 @@ +import * as p from '@clack/prompts'; +import * as fs from 'fs'; +import * as path from 'path'; +import { isInitialized, getOutputPath, ensureDirectoryExists } from '../utils/paths'; +import { convertToDocx, convertToPdf, isPandocInstalled, PandocFormat } from '../lib/pandoc'; + +interface ConvertOptions { + file?: string; + format?: string[]; + outputDir?: string; + outputName?: string; +} + +/** + * Recursively find all markdown files in a directory + */ +function findMarkdownFiles(dir: string): string[] { + const files: string[] = []; + + if (!fs.existsSync(dir)) { + return files; + } + + const entries = fs.readdirSync(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + + if (entry.isDirectory()) { + // Recursively search subdirectories + files.push(...findMarkdownFiles(fullPath)); + } else if (entry.isFile() && entry.name.endsWith('.md')) { + files.push(fullPath); + } + } + + return files; +} + +/** + * Get relative path from output directory for display + */ +function getRelativePath(fullPath: string, baseDir: string): string { + const relative = path.relative(baseDir, fullPath); + return relative || path.basename(fullPath); +} + +export async function convertCommand(options: ConvertOptions): Promise { + p.intro('Downfolio - Convert Document'); + + try { + if (!isInitialized()) { + p.cancel('Downfolio not initialized. Run "downfolio init" first.'); + process.exit(1); + } + + // Get input file path + let inputFilePath: string; + if (options.file) { + inputFilePath = options.file; + } else { + // List markdown files from output directory + const outputDir = getOutputPath(); + const markdownFiles = findMarkdownFiles(outputDir); + + if (markdownFiles.length === 0) { + p.cancel('No markdown files found in output directory. Generate documents first with "downfolio generate"'); + process.exit(1); + } + + // Create options for select prompt + const fileOptions = markdownFiles.map((file) => ({ + value: file, + label: getRelativePath(file, outputDir), + })); + + const selected = await p.select({ + message: 'Which markdown file to convert?', + options: fileOptions, + }); + + if (p.isCancel(selected)) { + p.cancel('Operation cancelled'); + process.exit(0); + } + inputFilePath = selected as string; + } + + // Validate input file exists + if (!fs.existsSync(inputFilePath)) { + p.cancel(`File not found: ${inputFilePath}`); + process.exit(1); + } + + // Validate it's a markdown file + if (!inputFilePath.endsWith('.md')) { + p.cancel('Input file must be a markdown file (.md)'); + process.exit(1); + } + + // Get output formats + let formats: PandocFormat[]; + if (options.format && options.format.length > 0) { + // Validate formats + const validFormats = options.format.filter((f) => f === 'docx' || f === 'pdf') as PandocFormat[]; + if (validFormats.length === 0) { + p.cancel('Invalid format. Must be "docx" and/or "pdf"'); + process.exit(1); + } + formats = validFormats; + } else { + const selected = await p.multiselect({ + message: 'Output format(s)?', + options: [ + { value: 'docx', label: 'Word (.docx)' }, + { value: 'pdf', label: 'PDF' }, + ], + required: true, + }); + + if (p.isCancel(selected)) { + p.cancel('Operation cancelled'); + process.exit(0); + } + formats = selected as PandocFormat[]; + } + + // Check if Pandoc is installed + const pandocInstalled = await isPandocInstalled(); + if (!pandocInstalled) { + p.cancel( + 'Pandoc is not installed. Please install Pandoc to convert to docx/pdf formats.\n' + + 'Installation: https://pandoc.org/installing.html' + ); + process.exit(1); + } + + // Determine output directory + const outputDir = options.outputDir + ? path.resolve(options.outputDir) + : path.dirname(inputFilePath); + + // Create output directory if it doesn't exist + ensureDirectoryExists(outputDir); + + // Determine output base name + const inputBaseName = path.basename(inputFilePath, '.md'); + const outputBaseName = options.outputName || inputBaseName; + + // Generate output paths + const outputPaths: { format: PandocFormat; path: string }[] = []; + if (formats.includes('docx')) { + outputPaths.push({ + format: 'docx', + path: path.join(outputDir, `${outputBaseName}.docx`), + }); + } + if (formats.includes('pdf')) { + outputPaths.push({ + format: 'pdf', + path: path.join(outputDir, `${outputBaseName}.pdf`), + }); + } + + // Check for existing files and prompt for confirmation + const existingFiles = outputPaths.filter((op) => fs.existsSync(op.path)); + if (existingFiles.length > 0) { + const fileList = existingFiles.map((op) => path.basename(op.path)).join(', '); + const confirmed = await p.confirm({ + message: `File(s) already exist: ${fileList}. Overwrite?`, + initialValue: false, + }); + + if (p.isCancel(confirmed)) { + p.cancel('Operation cancelled'); + process.exit(0); + } + + if (!confirmed) { + p.cancel('Conversion cancelled'); + process.exit(0); + } + } + + // Perform conversions + const spinner = p.spinner(); + spinner.start('Converting files...'); + + for (const outputPath of outputPaths) { + spinner.message(`Converting to ${outputPath.format.toUpperCase()}...`); + if (outputPath.format === 'docx') { + await convertToDocx(inputFilePath, outputPath.path); + } else if (outputPath.format === 'pdf') { + await convertToPdf(inputFilePath, outputPath.path); + } + } + + spinner.stop('Conversion complete'); + + const createdFiles = outputPaths.map((op) => path.basename(op.path)).join(', '); + p.outro(`Files created: ${createdFiles} → ${outputDir}/`); + } catch (error) { + p.cancel(`Conversion failed: ${error instanceof Error ? error.message : String(error)}`); + process.exit(1); + } +} diff --git a/src/commands/job.ts b/src/commands/job.ts index 54d82e0..8cdad82 100644 --- a/src/commands/job.ts +++ b/src/commands/job.ts @@ -187,6 +187,30 @@ async function handleRemove(options: JobOptions): Promise { process.exit(0); } - removeJob(jobToRemove.name); - p.log.success(`Job "${jobToRemove.name}" removed successfully`); + const filePath = removeJob(jobToRemove.name); + p.log.success(`Job "${jobToRemove.name}" removed from registry`); + + // Prompt to delete the file if it exists + if (filePath && fs.existsSync(filePath)) { + const deleteFile = await p.confirm({ + message: `Also delete the file "${path.basename(filePath)}"?`, + initialValue: false, + }); + + if (p.isCancel(deleteFile)) { + p.cancel('Operation cancelled'); + process.exit(0); + } + + if (deleteFile) { + try { + fs.unlinkSync(filePath); + p.log.success(`File "${path.basename(filePath)}" deleted`); + } catch (error) { + p.log.warn(`Failed to delete file: ${error instanceof Error ? error.message : String(error)}`); + } + } else { + p.log.info(`File "${path.basename(filePath)}" kept in directory`); + } + } } diff --git a/src/commands/template.ts b/src/commands/template.ts index c9a6067..d750c72 100644 --- a/src/commands/template.ts +++ b/src/commands/template.ts @@ -223,6 +223,30 @@ async function handleRemove(options: TemplateOptions): Promise { process.exit(0); } - removeTemplate(templateToRemove.name, templateToRemove.type); - p.log.success(`Template "${templateToRemove.name}" removed successfully`); + const filePath = removeTemplate(templateToRemove.name, templateToRemove.type); + p.log.success(`Template "${templateToRemove.name}" removed from registry`); + + // Prompt to delete the file if it exists + if (filePath && fs.existsSync(filePath)) { + const deleteFile = await p.confirm({ + message: `Also delete the file "${path.basename(filePath)}"?`, + initialValue: false, + }); + + if (p.isCancel(deleteFile)) { + p.cancel('Operation cancelled'); + process.exit(0); + } + + if (deleteFile) { + try { + fs.unlinkSync(filePath); + p.log.success(`File "${path.basename(filePath)}" deleted`); + } catch (error) { + p.log.warn(`Failed to delete file: ${error instanceof Error ? error.message : String(error)}`); + } + } else { + p.log.info(`File "${path.basename(filePath)}" kept in directory`); + } + } } diff --git a/src/lib/ai.ts b/src/lib/ai.ts index 48cf8af..99032fc 100644 --- a/src/lib/ai.ts +++ b/src/lib/ai.ts @@ -71,11 +71,14 @@ async function customizeWithOpenAI( max_tokens: 2000, }); - const content = completion.choices[0]?.message?.content; + let content = completion.choices[0]?.message?.content; if (!content) { throw new Error('No content returned from OpenAI API'); } + // Strip code block markers if AI wrapped the response in ``` + content = stripCodeBlockMarkers(content); + return { content, provider: 'openai', @@ -152,12 +155,15 @@ async function customizeWithAnthropic( } const data = await response.json() as { content?: Array<{ text?: string }> }; - const content = data.content?.[0]?.text; + let content = data.content?.[0]?.text; if (!content) { throw new Error('No content returned from Anthropic API'); } + // Strip code block markers if AI wrapped the response in ``` + content = stripCodeBlockMarkers(content); + return { content, provider: 'anthropic', @@ -171,6 +177,19 @@ async function customizeWithAnthropic( } } +/** + * Strip code block markers (```) from AI response if present + * Note: OpenAI models tend to wrap markdown responses in code blocks more often than Anthropic, + * but we strip them from both providers as a defensive measure. + */ +function stripCodeBlockMarkers(content: string): string { + // Remove leading ```markdown or ``` if present + content = content.replace(/^```(?:markdown)?\s*\n?/i, ''); + // Remove trailing ``` if present + content = content.replace(/\n?```\s*$/i, ''); + return content.trim(); +} + /** * Get system prompt based on document type */ @@ -186,7 +205,9 @@ Guidelines: - Maintain truthful representation of the candidate's background - Use action verbs and quantifiable achievements where possible - Keep the same structure and sections as the template -- Return ONLY the customized markdown content, no explanations or meta-commentary`; +- Return ONLY the customized markdown content, no explanations or meta-commentary +- Do NOT wrap the response in code blocks (no triple backticks) +- Return raw markdown text only`; } else { return `You are an expert cover letter writer specializing in personalized, compelling cover letters that connect candidate experiences to specific job opportunities. @@ -197,7 +218,9 @@ Guidelines: - Use a professional but personable tone - Highlight 2-3 key experiences or skills that directly relate to the job - Keep the same structure and style as the template -- Return ONLY the customized markdown content, no explanations or meta-commentary`; +- Return ONLY the customized markdown content, no explanations or meta-commentary +- Do NOT wrap the response in code blocks (no triple backticks) +- Return raw markdown text only`; } } diff --git a/src/lib/files.ts b/src/lib/files.ts index daff53a..7bf336d 100644 --- a/src/lib/files.ts +++ b/src/lib/files.ts @@ -79,7 +79,7 @@ export function listTemplates(): Template[] { return loadStorage